W lekcji 11.1 -- Wprowadzenie do funkcji przeciążenie, dowiedziałeś się o funkcji przeciążenie, które zapewnia mechanizm tworzenia i rozwiązywania wywołań funkcji do wielu funkcji o tej samej nazwie, o ile każda funkcja ma unikalny prototyp funkcji. Pozwala to na tworzenie odmian funkcji do pracy z różnymi typami danych, bez konieczności wymyślania unikalnej nazwy dla każdego wariantu.
W C++ operatory są implementowane jako funkcje. Używając przeciążania funkcji w funkcjach operatorowych, możesz zdefiniować własne wersje operatorów, które działają z różnymi typami danych (w tym z napisanymi przez Ciebie klasami). Używanie przeciążania funkcji do przeciążania operatorów nazywa się przeciążaniem operatora.
W tym rozdziale omówimy tematy związane z przeciążaniem operatorów.
Operatory jako funkcje
Rozważ następujący przykład:
int x { 2 };
int y { 3 };
std::cout << x + y << '\n';Kompilator ma wbudowaną wersję operatora plus (+) dla operandów całkowitych - ta funkcja dodaje razem liczby całkowite x i y i zwraca wynik w postaci całkowitej. Kiedy zobaczysz wyrażenie x + y, możesz przetłumaczyć to w swojej głowie na wywołanie funkcji operator+(x, y) (gdzie operator+ to nazwa funkcji).
Rozważmy teraz podobny fragment:
double z { 2.0 };
double w { 3.0 };
std::cout << w + z << '\n';Kompilator ma również wbudowaną wersję operatora plus (+) dla podwójnych operandów. Wyrażenie w + z staje się wywołaniem funkcji operator+(w, z), a przeciążenie funkcji służy do określenia, czy kompilator powinien wywołać podwójną wersję tej funkcji zamiast wersji całkowitej.
Zastanów się teraz, co się stanie, jeśli spróbujemy dodać dwa obiekty klasy zdefiniowanej w programie:
Mystring string1 { "Hello, " };
Mystring string2 { "World!" };
std::cout << string1 + string2 << '\n';Czego się spodziewasz, że się stanie w tym przypadku? Intuicyjnym oczekiwanym rezultatem jest ciąg „Hello, World!” zostanie wydrukowany na ekranie. Ponieważ jednak Mystring jest typem zdefiniowanym przez program, kompilator nie ma wbudowanej wersji operatora plus, którego może używać dla operandów Mystring. W tym przypadku zwróci nam to błąd. Aby to działało tak, jak chcemy, musielibyśmy napisać przeciążoną funkcję, która powie kompilatorowi, jak operator + powinien działać z dwoma operandami typu Mystring. Przyjrzymy się, jak to zrobić na następnej lekcji.
Rozwiązywanie przeciążonych operatorów
Podczas oceny wyrażenia zawierającego operator kompilator stosuje następujące zasady:
- Jeśli uniknąć operandy są podstawowymi typami danych, kompilator wywoła wbudowaną procedurę, jeśli taka istnieje. Jeśli taki nie istnieje, kompilator zgłosi błąd kompilatora.
- Jeśli dowolnego z operandów są typy zdefiniowane przez program (np. jedna z twoich klas lub typ wyliczeniowy), kompilator użyje algorytmu rozpoznawania przeciążenia funkcji (opisanego w lekcji 11.3 — Rozwiązywanie przeciążenia funkcji i dopasowania niejednoznaczne), aby sprawdzić, czy uda mu się znaleźć przeciążony operator, który jest jednoznacznie najlepiej dopasowany. Może to obejmować niejawną konwersję jednego lub większej liczby operandów w celu dopasowania typów parametrów przeciążonego operatora. Może to również obejmować niejawną konwersję typów zdefiniowanych przez program na typy podstawowe (za pomocą przeciążonego rzutowania typów, które omówimy w dalszej części tego rozdziału), aby dopasować je do operatora wbudowanego. Jeśli nie zostanie znalezione żadne dopasowanie (lub zostanie znalezione dopasowanie niejednoznaczne), kompilator wyświetli błąd.
Jakie są ograniczenia dotyczące przeciążania operatorów?
Po pierwsze, prawie każdy istniejący operator w C++ może zostać przeciążony. Wyjątkami są: warunek (?:), sizeof, zakres (::), selektor elementu członkowskiego (.), selektor elementu wskaźnikowego (.*), typeid i operatory rzutowania.
Po drugie, możesz przeciążać tylko istniejące operatory. Nie można tworzyć nowych operatorów ani zmieniać nazw istniejących operatorów. Na przykład nie można utworzyć operator** do wykonywania wykładników.
Po trzecie, co najmniej jeden z operandów w przeciążonym operatorze musi być typu zdefiniowanego przez użytkownika. Oznacza to, że możesz przeciążyć operator+(int, Mystring), ale nie operator+(int, double).
Ponieważ klasy bibliotek standardowych są uważane za zdefiniowane przez użytkownika, oznacza to, że możesz zdefiniować operator+(double, std::string). Nie jest to jednak dobry pomysł, ponieważ przyszły standard językowy mógłby zdefiniować to przeciążenie, co mogłoby spowodować uszkodzenie programów korzystających z Twojego przeciążenia. Zatem najlepszą praktyką jest to, że przeciążone operatory powinny działać na co najmniej jednym typie zdefiniowanym przez program. Gwarantuje to, że przyszły standard językowy nie uszkodzi potencjalnie Twoich programów.
Najlepsza praktyka
Przeciążony operator powinien operować na co najmniej jednym typie zdefiniowanym przez program (jako parametr funkcji lub ukryty obiekt).
Po czwarte, nie jest możliwa zmiana liczby operandów obsługiwanych przez operator.
W końcu wszystkie operatory zachowują swoje domyślne pierwszeństwo i łączność (niezależnie od tego, czego są używane) for) i nie można tego zmienić.
Niektórzy nowi programiści próbują przeciążać bitowy operator XOR (^), aby wykonać potęgowanie. Jednak w C++ operator^ ma niższy poziom pierwszeństwa niż podstawowe operatory arytmetyczne, co powoduje, że wyrażenia są obliczane niepoprawnie.
W podstawowej matematyce potęgowanie jest rozstrzygane przed podstawową arytmetyką, więc 4 + 3 ^ 2 jest rozpoznawane jako 4 + (3 ^ 2) => 4 + 9 => 13.
Jednak w C++ operatory arytmetyczne mają wyższy priorytet niż operator^, więc 4 + 3 ^ 2 jest rozpoznawane jako (4 + 3) ^ 2 => 7 ^ 2 => 49.
Musisz jawnie umieścić część wykładniczą w nawiasach (np. 4 + (3 ^ 2)) za każdym razem, gdy tego użyjesz, aby to działało poprawnie, co nie jest intuicyjne i potencjalnie podatne na błędy.
Ze względu na kwestię pierwszeństwa, ogólnie rzecz biorąc, dobrym pomysłem jest używanie operatorów wyłącznie w sposób analogiczny do ich pierwotnego przeznaczenia.
Najlepsza praktyka
W przypadku przeciążania operatorów najlepiej jest zachować funkcje operatorów możliwie najbliższe pierwotnemu zamierzeniu operatorów.
Ponadto, ponieważ operatory nie mają opisowych nazw, nie zawsze jest jasne, do czego są przeznaczone. Na przykład operator+ może być rozsądnym wyborem dla klasy łańcuchów do łączenia ciągów. Ale co z operatorem-? Czego byś się spodziewał? Nie jest to jasne.
Najlepsza praktyka
Jeśli znaczenie przeciążonego operatora nie jest jasne i intuicyjne, użyj zamiast niego funkcji nazwanej.
Wreszcie przeciążone operatory powinny zwracać wartości w sposób zgodny z oryginalnymi operatorami. Operatory, które nie modyfikują swoich operandów (np. operatory arytmetyczne), powinny generalnie zwracać wyniki według wartości. Operatory, które modyfikują swój lewy operand (np. preinkrementacja, dowolny operator przypisania) powinny generalnie zwracać skrajny lewy operand przez odwołanie.
Najlepsza praktyka
Operandy, które nie modyfikują swoich operandów (np. operatory arytmetyczne) powinny generalnie zwracać wyniki według wartości.
Operatory, które modyfikują swój skrajny lewy operand (np. przedinkrementacją, którykolwiek z operatorów przypisania) powinny generalnie zwracać skrajny lewy operand operand przez odniesienie.
W tych granicach nadal znajdziesz mnóstwo przydatnej funkcjonalności, którą możesz przeciążyć dla swoich niestandardowych klas! Możesz przeciążyć operator +, aby połączyć klasę łańcuchową zdefiniowaną w programie lub dodać razem dwa obiekty klasy Fraction. Możesz przeciążyć operator <<, aby ułatwić wydrukowanie klasy na ekranie (lub pliku). Możesz przeciążyć operator równości (==), aby porównać dwa obiekty klas. To sprawia, że przeciążanie operatorów jest jedną z najbardziej przydatnych funkcji w C++ — po prostu dlatego, że pozwala na bardziej intuicyjną pracę z klasami.
W nadchodzących lekcjach przyjrzymy się bliżej przeciążaniu różnych rodzajów operatorów.

