W przypadku klas, które mają wiele zmiennych składowych, drukowanie każdej indywidualnej zmiennej na ekranie może szybko stać się męczące. Rozważmy na przykład następującą klasę:
class Point
{
private:
double m_x{};
double m_y{};
double m_z{};
public:
Point(double x=0.0, double y=0.0, double z=0.0)
: m_x{x}, m_y{y}, m_z{z}
{
}
double getX() const { return m_x; }
double getY() const { return m_y; }
double getZ() const { return m_z; }
};Jeśli chcesz wydrukować instancję tej klasy na ekranie, musisz zrobić coś takiego:
Point point { 5.0, 6.0, 7.0 };
std::cout << "Point(" << point.getX() << ", " <<
point.getY() << ", " <<
point.getZ() << ')';Oczywiście rozsądniej jest zrobić to jako funkcję wielokrotnego użytku. W poprzednich przykładach widzieliśmy, jak tworzyliśmy print() funkcje, które działają w ten sposób:
class Point
{
private:
double m_x{};
double m_y{};
double m_z{};
public:
Point(double x=0.0, double y=0.0, double z=0.0)
: m_x{x}, m_y{y}, m_z{z}
{
}
double getX() const { return m_x; }
double getY() const { return m_y; }
double getZ() const { return m_z; }
void print() const
{
std::cout << "Point(" << m_x << ", " << m_y << ", " << m_z << ')';
}
};Chociaż jest to znacznie lepsze, nadal ma pewne wady. Ponieważ print() zwraca void, nie można go wywołać w środku instrukcji wyjściowej. Zamiast tego musisz zrobić tak:
int main()
{
const Point point { 5.0, 6.0, 7.0 };
std::cout << "My point is: ";
point.print();
std::cout << " in Cartesian space.\n";
}Byłoby znacznie łatwiej, gdybyś mógł po prostu wpisać:
Point point{5.0, 6.0, 7.0};
cout << "My point is: " << point << " in Cartesian space.\n";i uzyskać ten sam wynik. Nie byłoby podziału wyników na wiele instrukcji i nie trzeba byłoby pamiętać, jak nazwałeś funkcję print.
Na szczęście przeciążając operator<<, możesz!
Przeciążenie operator<<
Przeciążenie operator<< jest podobne do przeciążania operator+ (oba są operatorami binarnymi), z tą różnicą, że typy parametrów to inny.
Rozważ wyrażenie std::cout << point. Jeśli operatorem jest <<, jakie są operandy? Lewy operand to obiekt std::cout , a prawy operand to obiekt twojej Point klasy. std::cout jest w rzeczywistości obiektem typu std::ostream. Dlatego nasza przeciążona funkcja będzie wyglądać następująco:
// std::ostream is the type for object std::cout
friend std::ostream& operator<< (std::ostream& out, const Point& point);Implementacja operator<< dla naszej Point klasy jest dość prosta — ponieważ C++ już wie, jak wyprowadzać liczby podwójne za pomocą operator<<, a wszystkie nasze elementy są podwójne, możemy po prostu użyć operator<< do wypisywania elementów danych naszej Point. Oto powyższa Point klasa z przeciążoną operator<<.
#include <iostream>
class Point
{
private:
double m_x{};
double m_y{};
double m_z{};
public:
Point(double x=0.0, double y=0.0, double z=0.0)
: m_x{x}, m_y{y}, m_z{z}
{
}
friend std::ostream& operator<< (std::ostream& out, const Point& point);
};
std::ostream& operator<< (std::ostream& out, const Point& point)
{
// Since operator<< is a friend of the Point class, we can access Point's members directly.
out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ')'; // actual output done here
return out; // return std::ostream so we can chain calls to operator<<
}
int main()
{
const Point point1 { 2.0, 3.0, 4.0 };
std::cout << point1 << '\n';
return 0;
}To całkiem proste — zwróć uwagę, jak podobna jest nasza linia wyjściowa do linii w funkcji print() , którą napisaliśmy wcześniej. Najbardziej zauważalną różnicą jest to, że std::cout stał się parametrem out (który będzie odniesieniem do std::cout kiedy funkcja zostanie wywołana).
Najtrudniejszą częścią tutaj jest typ zwracanej wartości. Za pomocą operatorów arytmetycznych obliczyliśmy i zwróciliśmy pojedynczą odpowiedź według wartości (ponieważ tworzyliśmy i zwracaliśmy nowy wynik). Jeśli jednak spróbujesz zwrócić std::ostream według wartości, pojawi się błąd kompilatora. Dzieje się tak, ponieważ std::ostream w szczególności uniemożliwia kopiowanie.
W tym przypadku zwracamy lewy parametr jako odniesienie. To nie tylko zapobiega tworzeniu kopii std::ostream , ale także pozwala nam „połączyć” ze sobą polecenia wyjściowe, takie jak std::cout << point << '\n'.
Zastanawianie się, co by się stało, gdyby zamiast tego zwrócono nasze operator<< zwrócone void . Kiedy kompilator ocenia std::cout << point << '\n', ze względu na reguły pierwszeństwa/łączności, ocenia to wyrażenie, ponieważ (std::cout << point) << '\n';. std::cout << point wywołałoby naszą przeciążoną operator<< zwracającą pustkę funkcję, która zwraca void. Następnie częściowo obliczonym wyrażeniem staje się: void << '\n';, co nie ma sensu!
Zwracając zamiast tego parametr out jako typ zwracany, (std::cout << point) zwraca std::cout. Wtedy nasze częściowo ocenione wyrażenie stanie się: std::cout << '\n';, które następnie zostanie poddane ocenie!
Za każdym razem, gdy chcemy, aby nasze przeciążone operatory binarne można było łączyć w taki sposób, należy zwrócić lewy operand (przez odniesienie). Zwrócenie lewego parametru przez referencję jest w tym przypadku w porządku — ponieważ lewy parametr został przekazany przez funkcję wywołującą, musi nadal istnieć, gdy wywoływana funkcja powraca. Dlatego nie musimy się martwić odniesieniem do czegoś, co wyjdzie poza zakres i zostanie zniszczone, gdy operator powróci.
Aby udowodnić, że to działa, rozważ poniższy przykład, w którym zastosowano klasę Point z przeciążoną operator<< napisaną powyżej:
#include <iostream>
class Point
{
private:
double m_x{};
double m_y{};
double m_z{};
public:
Point(double x=0.0, double y=0.0, double z=0.0)
: m_x{x}, m_y{y}, m_z{z}
{
}
friend std::ostream& operator<< (std::ostream& out, const Point& point);
};
std::ostream& operator<< (std::ostream& out, const Point& point)
{
// Since operator<< is a friend of the Point class, we can access Point's members directly.
out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ')';
return out;
}
int main()
{
Point point1 { 2.0, 3.5, 4.0 };
Point point2 { 6.0, 7.5, 8.0 };
std::cout << point1 << ' ' << point2 << '\n';
return 0;
}Daje to następujący wynik:
Point(2, 3.5, 4) Point(6, 7.5, 8)
W powyższym przykładzie operator<< jest przyjacielem, ponieważ potrzebuje bezpośredniego dostępu do elementu członkowskiego z Point. Jeśli jednak dostęp do elementów można uzyskać za pomocą getterów, wówczas operator<< można by zaimplementować jako element inny niż przyjaciel.
Przeciążenie operator>>
Możliwe jest również przeciążenie operatora wejściowego. Odbywa się to w sposób analogiczny do przeciążania operatora wyjścia. Kluczową rzeczą, którą musisz wiedzieć jest to, że std::cin jest obiektem typu std::istream. Oto nasza Point klasa z przeciążoną operator>> dodaną:
#include <iostream>
class Point
{
private:
double m_x{};
double m_y{};
double m_z{};
public:
Point(double x=0.0, double y=0.0, double z=0.0)
: m_x{x}, m_y{y}, m_z{z}
{
}
friend std::ostream& operator<< (std::ostream& out, const Point& point);
friend std::istream& operator>> (std::istream& out, Point& point);
};
std::ostream& operator<< (std::ostream& out, const Point& point)
{
// Since operator<< is a friend of the Point class, we can access Point's members directly.
out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ')';
return out;
}
// note that point must be non-const so we can modify the object
std::istream& operator>> (std::istream& in, Point& point)
{
// This version subject to partial extraction issues (see below)
in >> point.m_x >> point.m_y >> point.m_z;
return in;
}
int main()
{
std::cout << "Enter a point: ";
Point point{ 1.0, 2.0, 3.0 }; // non-zero test data
std::cin >> point;
std::cout << "You entered: " << point << '\n';
return 0;
}Zakładając, że użytkownik wprowadzi 4.0 5.6 7.26 jako dane wejściowe, program wygeneruje następujący wynik:
You entered: Point(4, 5.6, 7.26)
Zobaczmy teraz, co się stanie, gdy użytkownik wprowadzi 4.0b 5.6 7.26 jako dane wejściowe (zwróć uwagę na b po 4.0):
You entered: Point(4, 0, 3)
Nasz punkt jest teraz dziwną hybrydą składającą się z jednej wartości wprowadzonej przez użytkownika (4.0), jednej wartości, która została zainicjowana zerem (0.0) i jednej wartości, która nie została dotknięta przez funkcję wejściową (3.0. To… niedobrze!
Ochrona przed częściową ekstrakcją
Kiedy będziemy. ekstrakcji pojedynczej wartości istnieją tylko dwa możliwe wyniki: ekstrakcja zakończy się niepowodzeniem lub zakończy się sukcesem. Jednak w przypadku wyodrębnienia więcej niż jednej wartości w ramach operacji wejściowej sytuacja staje się nieco bardziej skomplikowana.
Powyższa implementacja operator>> może skutkować częściową ekstrakcją. I właśnie to widzimy w przypadku input 4.0b 5.6 7.26. Ekstrakcja x_y pomyślnie wyodrębnia 4.0 z danych wejściowych użytkownika, pozostawiając b 5.6 7.26 w strumieniu wejściowym. Ekstrakcja do m_y nie udaje się wyodrębnić b, więc m_y jest przypisana wartość 0.0 , a strumień wejściowy przechodzi w tryb awaryjny. Ponieważ nie wyczyściliśmy trybu awarii, ekstrakcja m_z natychmiast przerywa, a wartość m_z miała przed próbą wyodrębnienia pozostaje (3.0).
Nie ma przypadku, w którym byłby to pożądany wynik. W niektórych przypadkach może to nawet być aktywnie niebezpieczne. Wyobraź sobie, że zamiast tego piszemy operator>> dla Fraction obiektu. Po pomyślnym wyodrębnieniu licznika, nieudane wyodrębnienie do mianownika spowoduje ustawienie mianownik do 0.0, co może później spowodować dzielenie przez zero i spowodować awarię aplikacji.
Jak więc możemy tego uniknąć? Jednym ze sposobów jest zapewnienie, aby nasze operacje były transakcyjne. operacja transakcyjna musi albo zakończyć się sukcesem, albo całkowicie zakończyć się niepowodzeniem — nie są dozwolone żadne częściowe sukcesy ani niepowodzenia. Jest to czasami określane jako „wszystko albo nic”. transakcji, należy cofnąć wcześniejsze zmiany dokonane tą operacją.
Kluczowa informacja
Transakcje zdarzają się w prawdziwym życiu cały czas. Rozważmy przypadek, w którym chcę przelać pieniądze z jednego konta bankowego na drugie. Wymaga to dwóch kroków: Najpierw pieniądze muszą zostać pobrane z jednego konta, a następnie przelane na drugie. W wykonaniu tej operacji istnieją trzy możliwości:
- Krok odliczenia kończy się niepowodzeniem (np. brak środków). transakcja nie powiedzie się i żadne saldo konta nie odzwierciedla przelewu.
- Etap kredytowania nie powiódł się (np. z powodu problemu technicznego). W takim przypadku odliczenie (które już się powiodło) należy cofnąć. Transakcja kończy się niepowodzeniem i żadne saldo konta nie odzwierciedla przelewu.
- Obydwa kroki powiodły się. Transakcja zakończyła się sukcesem i salda obu kont odzwierciedlają transfer.
Efekt końcowy jest taki, że istnieją tylko dwa możliwe wyniki: transfer całkowicie się nie powiedzie, a salda kont pozostaną niezmienione, lub transfer się powiedzie, a salda obu kont ulegną zmianie.
Zaimplementujmy ponownie nasze przeciążone Point operator>> jako operację transakcyjną:
// note that point must be non-const so we can modify the object
// note that this implementation is a non-friend
std::istream& operator>> (std::istream& in, Point& point)
{
double x{};
double y{};
double z{};
if (in >> x >> y >> z) // if all extractions succeeded
point = Point{x, y, z}; // overwrite our existing point
return in;
}W tej implementacji nie nadpisujemy elementów danych bezpośrednio dane wejściowe użytkownika. Zamiast tego wyodrębniamy dane wejściowe użytkownika do zmiennych tymczasowych (x, y, I z). Po zakończeniu wszystkich prób ekstrakcji sprawdzamy, czy wszystkie ekstrakcje powiodły się. Jeśli tak, to wspólnie zaktualizujemy wszystkich członków Point . W przeciwnym razie nie aktualizujemy żadnego z nich.
Wskazówka
if (in >> x >> y >> z) jest odpowiednikiem in >> x >> y >> z; if (in). Pamiętaj, że każda ekstrakcja zwraca in , dzięki czemu można połączyć wiele ekstrakcji ze sobą. Wersja z pojedynczą instrukcją wykorzystuje in zwróconą z ostatniej ekstrakcji jako warunek instrukcji if, podczas gdy wersja z wieloma instrukcjami wykorzystuje in jawnie.
Wskazówka
Operacje transakcyjne można implementować przy użyciu wielu różnych strategii. Na przykład:
- Zmień w przypadku powodzenia: Zapisz wynik każdej podoperacji. Jeśli wszystkie podoperacje zakończą się sukcesem, zastąp odpowiednie dane zapisanymi wynikami. To jest strategia, którą zastosowaliśmy w
Pointprzykładzie powyżej. - Przywracanie w przypadku niepowodzenia: Skopiuj wszystkie dane, które można zmienić. Jeśli jakakolwiek podoperacja zakończy się niepowodzeniem, zmiany wprowadzone w poprzednich podoperacjach mogą zostać cofnięte przy użyciu danych z kopii.
- Wycofanie w przypadku niepowodzenia: Jeśli jakakolwiek podoperacja zakończy się niepowodzeniem, każda poprzednia podoperacja zostanie cofnięta (przy użyciu przeciwnej podoperacji). Strategia ta jest często stosowana w bazach danych, gdzie dane są zbyt duże, aby utworzyć kopię zapasową, a wyników podoperacji nie można zapisać.
Powyższa operator>> zapobiega częściowej ekstrakcji, jest jednak niezgodna z działaniem operator>> dla typów podstawowych. Kiedy ekstrakcja do obiektu o typie podstawowym nie powiedzie się, obiekt nie pozostaje niezmieniony — jest kopiowany z przypisaną wartością 0 (dzięki temu obiekt ma pewną stałą wartość w przypadku, gdy nie został zainicjowany przed próbą wyodrębnienia). Dlatego, dla zachowania spójności, możesz chcieć, aby nieudana ekstrakcja resetowała obiekt do stanu domyślnego (przynajmniej w przypadkach, gdy coś takiego istnieje).
Oto alternatywna wersja operator>> , która resetuje Point do stanu domyślnego, jeśli jakakolwiek ekstrakcja się nie powiedzie:
// note that point must be non-const so we can modify the object
// note that this implementation is a non-friend
std::istream& operator>> (std::istream& in, Point& point)
{
double x{};
double y{};
double z{};
in >> x >> y >> z;
point = in ? Point{x, y, z} : Point{};
return in;
}Nota autora
Taka operacja nie jest już technicznie transakcyjna (ponieważ awaria nie „nic nie robi”). Nie wydaje się, aby istniał ogólny termin określający operacje gwarantujące brak częściowych rezultatów. Być może „operacja niepodzielna”.
Obsługa semantycznie nieprawidłowych danych wejściowych
Wyodrębnianie może zakończyć się niepowodzeniem.
W przypadkach, gdy operator>> po prostu nie uda się wyodrębnić niczego do zmiennej, std::cin zostanie automatycznie przełączony w tryb awaryjny (co omawiamy w lekcji 9.5 — std::cin i obsługa nieprawidłowych danych wejściowych). Osoba wywołująca tę funkcję może następnie sprawdzić std::cin , czy się nie powiodła, i odpowiednio potraktować ten przypadek.
Ale co z przypadkami, w których użytkownik wprowadza wartość, którą można wyodrębnić, ale semantycznie nieprawidłową (np. a Fraction z mianownikiem 0)? Ponieważ std::cin coś wyodrębniono, nie przejdzie ono automatycznie w tryb awarii. A wtedy osoba dzwoniąca prawdopodobnie nie zorientuje się, że coś poszło nie tak.
Aby rozwiązać ten problem, możemy przeciążyć nasze operator>> określ, czy którakolwiek z wyodrębnionych wartości jest semantycznie niepoprawna, a jeśli tak, ręcznie przełącz strumień wejściowy w tryb awaryjny. Można to zrobić, wywołując std::cin.setstate(std::ios_base::failbit);.
Oto przykład przeciążonego transakcyjnie operator>> for Point , które spowoduje, że strumień wejściowy przejdzie w tryb awarii, jeśli użytkownik wprowadzi wyodrębnialną wartość ujemną:
std::istream& operator>> (std::istream& in, Point& point)
{
double x{};
double y{};
double z{};
in >> x >> y >> z;
if (x < 0.0 || y < 0.0 || z < 0.0) // if any extractable input is negative
in.setstate(std::ios_base::failbit); // set failure mode manually
point = in ? Point{x, y, z} : Point{};
return in;
}Wnioski
Przeciążenie operator<< i operator>> ułatw wyświetlenie swojej klasy na ekranie i zaakceptuj dane wprowadzone przez użytkownika z konsoli.
Czas quizu
Pytanie nr 1
Weź poniższą klasę Fraction i dodaj do niego przeciążony operator<< i operator>> . Twoje operator>> powinno unikać częściowego wyodrębniania i kończyć się niepowodzeniem, jeśli użytkownik wprowadzi mianownik 0. Nie powinno resetować frakcji do ustawień domyślnych w przypadku awarii.
Powinien się skompilować następujący program:
int main()
{
Fraction f1{};
std::cout << "Enter fraction 1: ";
std::cin >> f1;
Fraction f2{};
std::cout << "Enter fraction 2: ";
std::cin >> f2;
std::cout << f1 << " * " << f2 << " is " << f1 * f2 << '\n'; // note: The result of f1 * f2 is an r-value
return 0;
}I wygeneruj wynik:
Enter fraction 1: 2/3 Enter fraction 2: 3/8 2/3 * 3/8 is 1/4
Oto klasa Fraction:
#include <iostream>
#include <numeric> // for std::gcd
class Fraction
{
private:
int m_numerator{};
int m_denominator{};
public:
Fraction(int numerator=0, int denominator=1):
m_numerator{numerator}, m_denominator{denominator}
{
// We put reduce() in the constructor to ensure any new fractions we make get reduced!
// Any fractions that are overwritten will need to be re-reduced
reduce();
}
void reduce()
{
int gcd{ std::gcd(m_numerator, m_denominator) };
if (gcd)
{
m_numerator /= gcd;
m_denominator /= gcd;
}
}
friend Fraction operator*(const Fraction& f1, const Fraction& f2);
friend Fraction operator*(const Fraction& f1, int value);
friend Fraction operator*(int value, const Fraction& f1);
void print() const
{
std::cout << m_numerator << '/' << m_denominator << '\n';
}
};
Fraction operator*(const Fraction& f1, const Fraction& f2)
{
return Fraction { f1.m_numerator * f2.m_numerator, f1.m_denominator * f2.m_denominator };
}
Fraction operator*(const Fraction& f1, int value)
{
return Fraction { f1.m_numerator * value, f1.m_denominator };
}
Fraction operator*(int value, const Fraction& f1)
{
return Fraction { f1.m_numerator * value, f1.m_denominator };
}Jeśli korzystasz z kompilatora w wersji starszej niż C++17, możesz zastąpić std::gcd tą funkcją:
#include <cmath>
int gcd(int a, int b) {
return (b == 0) ? std::abs(a) : gcd(b, a % b);
}
