W poprzedniej lekcji (13.4 -- Konwersja wyliczenia na i z ciągu znaków), pokazaliśmy ten przykład, w którym użyliśmy funkcji do konwersji wyliczenia na równoważny ciąg znaków:
#include <iostream>
#include <string_view>
enum Color
{
black,
red,
blue,
};
constexpr std::string_view getColorName(Color color)
{
switch (color)
{
case black: return "black";
case red: return "red";
case blue: return "blue";
default: return "???";
}
}
int main()
{
constexpr Color shirt{ blue };
std::cout << "Your shirt is " << getColorName(shirt) << '\n';
return 0;
}Chociaż powyższy przykład działa dobrze, istnieją dwie wady:
- Musimy pamiętać nazwa funkcji, którą utworzyliśmy, aby uzyskać nazwę modułu wyliczającego.
- Konieczność wywołania takiej funkcji powoduje bałagan w naszej instrukcji wyjściowej.
Idealnie byłoby, gdybyśmy mogli w jakiś sposób nauczyć operator<< wyprowadzania wyliczenia, abyśmy mogli zrobić coś takiego: std::cout << shirt i sprawić, by robiła to, czego oczekujemy.
Wprowadzenie do przeciążenia operatorów
W lekcji 11.1 -- Wprowadzenie do funkcji przeciążenie, wprowadziliśmy przeciążanie funkcji, które pozwala nam utwórz wiele funkcji o tej samej nazwie, o ile każda funkcja ma unikalny prototyp funkcji. Korzystając z przeciążania funkcji, możemy tworzyć odmiany funkcji, które działają z różnymi typami danych, bez konieczności wymyślania unikalnej nazwy dla każdego wariantu.
Podobnie, C++ obsługuje również przeciążaniem operatora, co pozwala nam definiować przeciążenia istniejących operatorów, dzięki czemu możemy sprawić, że operatory te będą działać z naszymi typami danych zdefiniowanymi w programie.
Podstawowe przeciążanie operatorów jest dość rozsądne proste:
- Zdefiniuj funkcję, używając nazwy operatora jako nazwy funkcji.
- Dodaj parametr odpowiedniego typu dla każdego operandu (w kolejności od lewej do prawej). Jeden z tych parametrów musi być typem zdefiniowanym przez użytkownika (typ klasowy lub typ wyliczeniowy), w przeciwnym razie kompilator popełni błąd.
- Ustaw typ zwracany na dowolny typ, który ma sens.
- Użyj instrukcji return, aby zwrócić wynik operacji.
Gdy kompilator napotka użycie operatora w wyrażeniu, a jeden lub więcej operandów jest typem zdefiniowanym przez użytkownika, kompilator sprawdzi, czy jeśli istnieje przeciążona funkcja operatora, której można użyć do rozwiązania tego wywołania. Na przykład, biorąc pod uwagę pewne wyrażenie x + y, kompilator użyje rozpoznawania przeciążenia funkcji, aby sprawdzić, czy istnieje wywołanie funkcji operator+(x, y) , którego może użyć do oceny operacji. Jeśli zostanie znaleziona niejednoznaczna operator+ funkcja, zostanie ona wywołana, a wynik operacji zwrócony jako wartość zwracana.
Powiązana treść
Przeciążanie operatorów omówimy bardziej szczegółowo w rozdziale rozdział 21.
Dla zaawansowanych czytelników
Operatory mogą być również przeciążane jako funkcje składowe lewego argumentu. Omawiamy to na lekcji 21.5 — Przeciążanie operatorów przy użyciu funkcji składowych.
Przeciążenie operator<< w celu wydrukowania modułu wyliczającego
Zanim przejdziemy dalej, szybko podsumujmy, jak operator<< działa, gdy są używane do wyświetlania danych wyjściowych.
Rozważ proste wyrażenie, takie jak std::cout << 5. std::cout ma typ std::ostream (które jest typem zdefiniowanym przez użytkownika w standardowej bibliotece), oraz 5 jest literałem typu int.
Podczas oceny tego wyrażenia kompilator będzie szukać przeciążonej operator<< funkcji, która może obsłużyć argumenty typu std::ostream i int. Znajdzie taką funkcję (również zdefiniowaną jako część standardowej biblioteki I/O) i wywoła ją. Wewnątrz tej funkcji std::cout służy do wysyłania x wyjścia na konsolę (dokładnie tak, jak zdefiniowano w implementacji). Na koniec funkcja operator<< zwraca swój lewy operand (którym w tym przypadku jest std::cout), dzięki czemu kolejne wywołania operator<< mogą być łączone w łańcuch.
Mając na uwadze powyższe, zaimplementujmy przeciążenie operator<< aby wydrukować Color:
#include <iostream>
#include <string_view>
enum Color
{
black,
red,
blue,
};
constexpr std::string_view getColorName(Color color)
{
switch (color)
{
case black: return "black";
case red: return "red";
case blue: return "blue";
default: return "???";
}
}
// Teach operator<< how to print a Color
// std::ostream is the type of std::cout, std::cerr, etc...
// The return type and parameter type are references (to prevent copies from being made)
std::ostream& operator<<(std::ostream& out, Color color)
{
out << getColorName(color); // print our color's name to whatever output stream out
return out; // operator<< conventionally returns its left operand
// The above can be condensed to the following single line:
// return out << getColorName(color)
}
int main()
{
Color shirt{ blue };
std::cout << "Your shirt is " << shirt << '\n'; // it works!
return 0;
}Wypisuje:
Your shirt is blue
Rozpakujmy trochę naszą przeciążoną funkcję operatora. Po pierwsze, nazwa funkcji to operator<<, ponieważ jest to nazwa operatora, który przeciążamy. operator<< ma dwa parametry. Lewy parametr (który zostanie dopasowany do lewego operandu) to nasz strumień wyjściowy, który ma typ std::ostream. Używamy tutaj referencji typu pass by non-const, ponieważ nie chcemy tworzyć kopii obiektu std::ostream po wywołaniu funkcji, ale std::ostream obiekt musi zostać zmodyfikowany, aby móc wykonać dane wyjściowe. Właściwym parametrem (który zostanie dopasowany do odpowiedniego operandu) jest nasz Color obiekt. Ponieważ operator<< konwencjonalnie zwraca swój lewy operand, typ zwracany jest zgodny z typem lewego operandu, czyli std::ostream&.
Przyjrzyjmy się teraz implementacji. Obiekt std::ostream wie już jak wydrukować std::string_view za pomocą operator<< (jest to część standardowej biblioteki). Zatem out << getColorName(color) po prostu pobiera nazwę naszego koloru jako a std::string_view , a następnie wypisuje ją do strumienia wyjściowego.
Zauważ, że nasza implementacja używa parametru out zamiast std::cout , ponieważ chcemy pozwolić wywołującemu określić, do którego strumienia wyjściowego będzie wysyłany (np. std::cerr << color powinien wysyłać do std::cerr, nie std::cout).
Zwrócenie lewego operandu to również łatwe. Lewy operand to parametr out, więc po prostu wracamy out.
Składając wszystko w całość: kiedy wywołamy std::cout << shirt, kompilator zobaczy, że jesteśmy przeciążeni operator<< do pracy z obiektami typu Color. Nasza przeciążona funkcja operator<< jest następnie wywoływana z std::cout as parametr out , a nasza shirt zmienna (która ma wartość blue) jako parametr color. Ponieważ out jest odniesieniem do std::cout, I color jest kopią modułu wyliczającego blue, wyrażenie out << getColorName(color) wypisuje "blue" do konsoli Na koniec out jest zwracane z powrotem do wywołującego, jeśli tego chcemy łańcuch dodatkowych wyników.
Przeciążenie operator>> wprowadzanie wyliczenia
Podobnie jak nauczyliśmy operator<< wyprowadzania wyliczenia powyżej, możemy również nauczyć operator>> jak wprowadzić wyliczenie:
#include <iostream>
#include <limits>
#include <optional>
#include <string>
#include <string_view>
enum Pet
{
cat, // 0
dog, // 1
pig, // 2
whale, // 3
};
constexpr std::string_view getPetName(Pet pet)
{
switch (pet)
{
case cat: return "cat";
case dog: return "dog";
case pig: return "pig";
case whale: return "whale";
default: return "???";
}
}
constexpr std::optional<Pet> getPetFromString(std::string_view sv)
{
if (sv == "cat") return cat;
if (sv == "dog") return dog;
if (sv == "pig") return pig;
if (sv == "whale") return whale;
return {};
}
// pet is an in/out parameter
std::istream& operator>>(std::istream& in, Pet& pet)
{
std::string s{};
in >> s; // get input string from user
std::optional<Pet> match { getPetFromString(s) };
if (match) // if we found a match
{
pet = *match; // dereference std::optional to get matching enumerator
return in;
}
// We didn't find a match, so input must have been invalid
// so we will set input stream to fail state
in.setstate(std::ios_base::failbit);
// On an extraction failure, operator>> zero-initializes fundamental types
// Uncomment the following line to make this operator do the same thing
// pet = {};
return in;
}
int main()
{
std::cout << "Enter a pet: cat, dog, pig, or whale: ";
Pet pet{};
std::cin >> pet;
if (std::cin) // if we found a match
std::cout << "You chose: " << getPetName(pet) << '\n';
else
{
std::cin.clear(); // reset the input stream to good
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cout << "Your pet was not valid\n";
}
return 0;
}Istnieje kilka różnic w stosunku do przypadku wyjściowego, o których warto tutaj wspomnieć. Po pierwsze, std::cin ma typ std::istream, więc my użyj std::istream& jako typu naszego lewego parametru i zwróć wartość zamiast std::ostream&. Po drugie, parametr pet nie jest stałą referencją. To pozwala naszemu operator>> zmodyfikować wartość prawego operandu, który jest przekazywany, jeśli nasze wyodrębnienie zakończy się dopasowaniem.
Kluczowa informacja
Nasz prawy operand (pet) jest parametrem wyjściowym opisz parametry w lekcji 12.13 — Parametry wejścia i wyjścia.
Jeśli pet były parametrem wartości, a nie parametrem odniesienia, wtedy nasza operator>> funkcja przypisałaby nową wartość do kopii prawego operandu, a nie do rzeczywistego prawego operandu. W tym przypadku chcemy, aby nasz prawy operand został zmodyfikowany.
Wewnątrz funkcji użyj operator>> wprowadź std::string (coś, co już wie, jak to zrobić). Jeśli wartość, którą użytkownik wprowadzi, pasuje do jednego z naszych zwierzaków, wówczas możemy przypisać pet odpowiedni moduł wyliczający i zwrócić lewy operand (in).
Jeśli użytkownik nie wprowadził prawidłowego zwierzaka, radzimy sobie z tym przypadkiem, wprowadzając std::cin w „tryb awaryjny”. Jest to stan, który std::cin zwykle jest uruchamiany w przypadku niepowodzenia wyodrębniania, wywołujący może następnie sprawdzić std::cin , czy wyodrębnienie powiodło się, czy nie.
Powiązana treść
W lekcji 17.6 — std::array i wyliczenia pokazujemy, jak możemy użyć std::array , aby zmniejszyć redundancję naszych operatorów wejścia i wyjścia i uniknąć konieczności ich modyfikowania po dodaniu nowego modułu wyliczającego.

