Menu

Przeciążanie operatorów w C++: własne +, == i <<

Przeciążanie operatorów w C++ pozwala twoim typom działać z wbudowanymi operatorami takimi jak +, == i <<. Poznaj zasady wyboru między składową a funkcją spoza klasy, przeciążanie porównań i operatorów strumienia oraz pułapki związane z typem zwracanym i operatorem przypisania.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Twoje typy jak wbudowane

Wiesz już, że std::string pozwala napisać a + b, żeby połączyć napisy, i cout << s, żeby wypisać. To nie są specjalne sztuczki kompilatora, tylko zwykłe funkcje o zabawnych nazwach. Przeciążanie operatorów to mechanizm, który pozwala twoim klasom korzystać z tej samej składni, więc typ Vector2 czy Money można dodawać, porównywać i wypisywać dokładnie jak int.

Mechanizm jest prosty, gdy już go zobaczysz: wyrażenie takie jak a + b to skrót. Kompilator przepisuje je jako wywołanie funkcji o nazwie operator+ i szuka takiej, która pasuje do typów operandów. Zdefiniuj tę funkcję dla swojej klasy, a a + b nagle zadziała. To w gruncie rzeczy wyspecjalizowana forma przeciążania funkcji: obowiązują te same zasady rozstrzygania nazw, tylko nazwy mają kształt operatorów.

Zauważ, że funkcja przyjmuje oba operandy przez const&: arytmetyka nie powinna zmieniać swoich argumentów, a referencje pozwalają uniknąć kopiowania. Zwraca nowy Vector2 przez wartość: p + q musi dać świeży wynik bez ruszania p ani q, tak jak 2 + 3 nie zmienia 2.

Metoda czy funkcja spoza klasy

Operator można zdefiniować w dwóch miejscach: jako składową klasy albo jako wolną funkcję (spoza klasy). Jako składowa lewy operand to niejawne this, więc operator dwuargumentowy przyjmuje tylko jeden jawny parametr:

const po liście parametrów ma znaczenie: a + b nie powinno zmieniać a, więc metoda jest oznaczona jako const. Formy składowej używaj dla operatorów nierozerwalnie związanych z lewym operandem, które nie potrzebują dla niego konwersji: +=, [], (), -> oraz operatory jednoargumentowe, takie jak -x czy ++x.

Haczyk przy składowych: lewego operandu nie da się skonwertować. Przy składowym operator+ powyżej a + 50 działa (50 konwertuje się na Money po prawej stronie), ale 50 + a się nie kompiluje: lewy operand 50 to int, a do int nie da się dodać metody. Operator spoza klasy to naprawia, bo oba operandy są jawnymi parametrami i oba mogą być konwertowane:

Praktyczna zasada: symetryczne operatory dwuargumentowe (+, ==, *) rób spoza klasy, żeby konwersje działały po obu stronach; operatory, które muszą modyfikować lewy operand albo są z nim związane (+=, [], =), rób składowymi.

Przeciążanie operatora strumienia

Najczęściej przeciążany operator to << do wypisywania. Nie możesz zrobić go składową swojej klasy, bo lewy operand to std::ostream (jak cout), a nie twój typ, a ostream nie należy do ciebie. Dlatego to zawsze funkcja spoza klasy, która przyjmuje strumień przez referencję niebędącą const i go zwraca:

Działanie zapewniają dwa szczegóły. Strumień jest przekazywany i zwracany przez referencję (ostream&): strumieni nie da się kopiować, a zwrócenie tego samego strumienia pozwala łączyć cout << "p = " << p << "\n". Każde << zwraca strumień, więc kolejne << ma się do czego podpiąć. Zapomnij o return os;, a łączenie przestanie działać.

Operatory porównania

Aby porównywać obiekty przez ==, < i pozostałe, przeciąż operatory porównania. Przed C++20 pisało się każdy ręcznie; kluczowa pułapka polega na tym, że operator< musi zwracać bool i definiować spójny porządek:

Ręczne pisanie wszystkich sześciu porównań (==, !=, <, <=, >, >=) jest żmudne i podatne na błędy. C++20 dodał operator porównania trójdrożnego <=> (tzw. „spaceship”). Domyślna wersja tego operatora razem z == generuje za ciebie wszystkie porównania:

= default każe kompilatorowi porównywać składowe w kolejności deklaracji, co daje dokładnie ten porządek leksykograficzny, który powstałby przy ręcznym pisaniu. Na nowoczesnych kompilatorach wybieraj to rozwiązanie.

Operator przypisania i jego pułapki

operator= (przypisanie kopiujące) jest wyjątkowy: kompilator generuje go za ciebie, a dla prostych klas ta domyślna wersja jest poprawna. Własny musisz napisać tylko wtedy, gdy klasa zarządza zasobem, takim jak surowa pamięć czy uchwyt pliku, bo wtedy kopiowanie składowa po składowej byłoby błędne. Kanoniczna sygnatura zwraca *this przez referencję, żeby przypisania dało się łączyć (a = b = c):

W tej krótkiej funkcji kryją się dwie pułapki. Po pierwsze sprawdzenie samoprzypisania if (this == &other): bez niego a = a wykonałoby delete[] data, a potem czytało z właśnie zwolnionego other.data, co jest niezdefiniowanym zachowaniem. Po drugie liczy się kolejność: w ręcznie napisanej wersji nie wolno usunąć starego bufora, zanim bezpiecznie skopiujesz nowy (prawdziwa implementacja często najpierw alokuje albo używa idiomu copy-and-swap, żeby nieudana alokacja zostawiła obiekt nienaruszony).

Szersza pułapka: nie przeciążaj operatorów w zaskakujący sposób. operator+, który potajemnie modyfikuje lewy operand, albo operator==, który nie jest symetryczny, zmyli każdego czytelnika i zepsuje kod biblioteki standardowej, który zakłada zwykłe znaczenia. Przeciążaj operatory tylko wtedy, gdy operacja jest dla twojego typu naprawdę „jak dodawanie” albo „jak równość”.

Dalej: specyfikatory dostępu

Zauważ, że każdy przykład trzymał pola jako private i udostępniał zachowanie przez niewielki publiczny interfejs: konstruktory, operatory i kilka metod. Granicą między tym, co widoczne dla świata zewnętrznego, a tym, co ukryte w klasie, sterują specyfikatory dostępu: public, private i protected. Dalej zobaczysz, na co dokładnie pozwala każdy z nich, dlaczego dane private z publicznymi metodami to domyślny wybór dla dobrej hermetyzacji i jak protected wpasowuje się w dziedziczenie.

Najczęściej zadawane pytania

Czym jest przeciążanie operatorów w C++?

Przeciążanie operatorów pozwala określić, co wbudowane operatory takie jak +, == czy << znaczą dla twoich własnych typów. Piszesz funkcję o specjalnej nazwie, np. operator+ albo operator==, a kompilator wywołuje ją zawsze, gdy operator pojawia się z operandami twojej klasy. Dzięki temu string + string łączy napisy, a cout << obj wypisuje własny obiekt.

Czy operatory w C++ powinny być metodami, czy funkcjami spoza klasy (friend)?

Użyj metody, gdy lewy operand to twoja klasa i nie wymaga konwersji (np. +=, [], ()). Użyj funkcji spoza klasy (często friend), gdy lewy operand może być typem wbudowanym albo gdy chcesz symetrycznych konwersji po obu stronach. Tak musi być przy operator<<, bo lewy operand to std::ostream, a nie twoja klasa.

Których operatorów w C++ nie można przeciążyć?

Nie można przeciążyć :: (rozstrzyganie zasięgu), . (dostęp do składowej), .* (dostęp przez wskaźnik do składowej), ?: (operator trójargumentowy) ani sizeof. Nie można też wymyślać zupełnie nowych operatorów ani zmieniać liczby argumentów czy priorytetu operatora: + jest zawsze dwuargumentowy z tym samym priorytetem, niezależnie od tego, czy dodaje inty, czy twoje Vector2.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ