18.1 Wstęp

Użycie w tekście programu operatorów (takich jak ' +', ' ->', ' &&' itd.) powoduje tak naprawdę wywołanie funkcji z argumentami będącymi wartościami wyrażeń stojących po obu ich stronach (lub po jednej, dla operatorów jednoargumentowych). Tak więc podczas opracowania instrukcji

       c = a + b;
wywołana zostanie funkcja mająca na celu dodanie  ab. Jaka to będzie funkcja, zależy od typu ab; jeśli oba argumenty są typu double, to wywołana zostanie inna funkcja niż ta służąca do dodania dwóch liczb typu int. Jeśli  a jest typu int, a  b typu double, to, jak wiemy, najpierw wartość zmiennej  a (ale nie sama zmienna!) zostanie skonwertowana do typu double i następnie zostanie wywołana funkcja dodająca wartości typu double (i wynik będzie też tego typu). Automatyczne konwersje i wybór odpowiedniej funkcji będą przebiegać „samoczynnie” dla typów wbudowanych. Dla argumentów będących obiektami klas przez nas samych zdefiniowanych, znaczenie tych operatorów jest jednak niezdefiniowane. Zatem, jeśli chcemy korzystać z tych operatorów w takim kontekście, musimy je sami określić.

Możemy to zrobić określając (w specjalny sposób) w definiowanej przez nas klasie odpowiednie metody lub definiując przeznaczone do tego celu funkcje globalne, najczęściej, choć nie jest to konieczne, zaprzyjaźnione z naszą klasą.

Taką operację nazywamy przeciążaniem lub przeładowaniem operatora, w analogi do przeciążania funkcji, czyli definiowania kilku funkcji o tej samej nazwie. Tak jak dla funkcji, właściwa wersja operatora znajdowana jest przez kompilator na podstawie typu argumentów.

Operatory, które mogą być przeciążone (przeładowane), to:

Tabela: Operatory które mogą być przeciążone
+ - * / % ^ & | !
= < > += -= *= /= %= ^= &=
|= $ \ll$ $ \gg$ $ \ll$= $ \gg$= == != <= >= &&
$ \Vert$ ++ - - , ->* -> new delete () []

przy czym operatory ' &', ' *', ' -', ' +' mogą być przeładowane w obu wersjach: jedno- i dwuargumentowej. Jednoargumentowe operatory zwiększenia i zmniejszenia, '++' i '- -', można przeładowywać z kolei w obu ich odmianach: przyrostkowej i przedrostkowej. Operatory ' =', ' ->',' ()' i ' []' są w pewien sposób specjalne i nimi zajmiemy się osobno. Przeciążać można też operatory ' new' i ' delete', ale jest to kwestia dość delikatna i często zależna od implementacji; nie będziemy się zatem nią zajmować.

Nie można natomiast przeciążać operatorów ' .', ' .*', ' ::', ' ?:' i  sizeof.

Niektóre operatory są dostarczane przez system dla (prawie) każdej klasy, nawet jeśli sami nie przeciążyliśmy ich w żaden sposób. Należą do nich operatory ' &' (pobrania adresu), ' =' (przypisania), ' ,' (przecinek) oraz newdelete. Pierwszego lepiej nie przeciążać, ostatnich trzech zwykle nie ma potrzeby.


Jakkolwiek nie przeciążalibyśmy operatorów, ich priorytety i sposób wiązania (od lewej do prawej lub od prawej do lewej, patrz rozdział o operatorach ) pozostają takie same jak dla standardowych operatorów oznaczonych tym samym symbolem. Tak więc w wyrażeniu

       a = b + c * d;
operacja przypisana symbolowi ' *' zastosowana zostanie przed operacją przypisaną symbolowi ' +', niezależnie od tego, jak zdefiniowane są w tym kontekście te dwie operacje. Podobnie, niezależnie od tego, jak przedefiniujemy operator przypisania ' =', wyrażenie
       a = b = c;
jest równoważne wyrażeniu
       a = ( b = c );
a nie
       ( a = b ) = c;
bo operator przypisania wiąże od prawej.

Również liczba argumentów przeciążonego operatora nie jest dowolna: istnieje na przykład tylko jedna standardowa wersja operatora '%' (reszta z dzielenia), w której operator ten jest dwuargumentowy. Nie można zatem przeciążyć tego operatora tak, aby był jednoargumentowy. A więc operatory dwuargumentowe w wersji przeładowanej muszą być też dwuargumentowe, a jednoargumentowe —  jednoargumentowe.


Przeładowując operatory należy starać się stosować zasadę of least astonishment — najmniejszego zaskoczenia —  tzn. tak przeładowywać operatory, by można było łatwo przewidzieć ich zachowanie na podstawie analogii z normalnym znaczeniem danego operatora. Na przykład, jeśli przeładowywujemy operator dodawania, to zwracany wynik powinien być typu tego samego co argumenty i powinien nie być l-wartością (a zatem na przykład nie powinien zwracać wyniku przez referencję). Dodawanie bowiem nie powinno „dać się postawić” po lewej stronie przypisania.

Podobnie, jeśli przeładowaliśmy operator ' +', to powinniśmy przeciążyć również operator ' +=' i pomyśleć, czy nie byłoby naturalne przeładować też odejmowanie ('-'), odejmowanie z przypisaniem (' -=') i/lub operatory zmniejszenia i zwiększenia.

Z drugiej strony, gdybyśmy definiowali klasę String naśladującą klasę String lub StringBuilder z Javy, naturalne byłoby zastąpienie funkcji scalającej napisy (konkatenacji) przeładowanym operatorem dodawania: w tym przykładzie nie byłoby natomiast intuicyjnie oczywistej interpretacji odejmowania czy zmniejszania.

T.R. Werner, 23 lutego 2019; 23:59