). Powinniśmy
jednakże pamiętać tu o losie programisty — serwisanta kodów. Nie powinniśmy doprowadzać
do tego, by zadanie polegające na zrozumieniu naszych kodów było trudniejsze, niż jest to
rzeczywiście konieczne.
Wbudowana w C++ możliwość przeciążania operatorów stanowi znaczący wkład do estetyki
programowania komputerów.
Elastyczność C++ ma swoją cenę. Jak zawsze — coś za coś. Jeśli chcemy zainicjować je-
den obiekt, posłużywszy się w tym celu innym obiektem (przy definiowaniu obiektu, przy
przekazywaniu go jako parametru poprzez wartość lub przy zwrocie obiektu poprzez wartość
z funkcji), powinniśmy zdecydowanie dodać do klasy konstruktor kopii. Jeśli chcemy dokonać
przypisania jednego obiektu innemu obiektowi, powinniśmy zdecydowanie dodać do specyfi-
kacji klasy funkcję operatorową dokonującą przeciążenia operatora przypisania.
Problemy z zachowaniem integralności programu, które mogą wystąpić z powodu stosowa-
nia nieklarownego dynamicznego zarządzania pamięcią, są na tyle niebezpieczne, że wielu
programistów implementuje konstruktor kopiujący i funkcję operatorową operatora przypisa-
nia dla każdej klasy, która dynamicznie zarządza pamięcią. Programiści robią to często na-
wet w takich klasach, które nie zarządzają pamięcią w sposób dynamiczny. W końcu napisanie
tych funkcji nie wymaga wiele wysiłku — zawsze lepiej to zrobić, choćby tylko tak, na
wszelki wypadek.
Autor uważa, że to jest problem z kategorii „dmuchania na zimne”. Zamiast dodawać do kodu
wiele kompletnie bezużytecznych funkcji, projektant powinien uważnie przeanalizować rze-
czywiste wymagania kodu klienta i zrozumieć konsekwencje różnorodnych decyzji pro-
jektowych.
Z dostarczaniem klas z ilością metod przewyższającą tę, której klasa rzeczywiście potrze-
buje, wiążą się liczne problemy. Jednym z nich jest rozdęty ponad miarę projekt. To nie jest
mało znacząca konsekwencja. Gdy serwisant kodów (lub programista piszący kod klienta)
przegląda kody bezużytecznych funkcji, nie zwraca uwagi na inne istotne szczegóły.
Inną kwestią jest tu efektywność działania kodu. Jak widać na rysunku 11.18, problem ten
może stać się zupełnie realny. Dla każdego przypisania wejściowego łańcucha znaków w ob-
rębie pętli programowej potrzebne są dwa wywołania funkcji oraz wywołanie funkcji opera-
torowej przeciążającej operator przypisania:
1. Wywołanie konstruktora konwersji wobec argumentu funkcji operatorowej .
2. Wywołanie samej funkcji operatorowej .
610
Część II n Programowanie obiektowe w C++
3. Wywołanie konstruktora kopii w celu zwrócenia obiektu poprzez wartość z wnętrza
funkcji operatorowej.
Pomimo tych wysiłków nadal występują znaczne różnice pomiędzy traktowaniem obiektów
klas definiowanych przez programistę a traktowaniem zmiennych typów wbudowanych.
Gdyby tablice 56 oraz 56 składały się z elementów typu elementarnego, wewnątrz naszej
pętli programowej mogłaby się znajdować tylko jedna, pojedyncza instrukcja. Przy takiej
konstrukcji klasy wnętrze tej pętli programowej reprezentuje coś zupełnie innego —
aż trzy wywołania funkcji.
1 (#$ ( SNT (22
=(>#=(> , " =(> #; =(>
Zwróć uwagę, że każda z takich operacji jest dość kosztowna (w sensie czasu wykonania).
Poza tym oprócz wywołań funkcji każda z tych operacji pociąga za sobą konieczność przy-
działu pamięci na stercie, skopiowania parametru — łańcucha znaków do pamięci przy-
dzielonej na stercie, a następnie, podczas wywołania destruktora, zwolnienia tej pamięci
i jej zwrotu do systemu. Wykonanie tych wszystkich działań jest nieuniknione jeden raz —
dla operatora przypisania, który posługuje się semantyką wartości w celu utrzymywania pamięci
na stercie odrębnie dla swoich dwu operandów. Jednak robić to jeszcze dwa razy? Raz — wo-
bec parametru funkcji operatorowej przeciążającej operator przypisania. Drugi raz — wobec
wartości zwracanej z tej funkcji. Wygląda na to, że to zbyt wiele. Aby wyczerpać ten kłopo-
tliwy temat do końca dodajmy, że obiekt tworzony przez konstruktor kopiujący nie jest
używany przez kod klienta (przypomnę, że zwrot obiektu z funkcji został wprowadzony
tylko po to, by poprawnie obsługiwać łańcuchowe operacje przypisania). Zostaje całkowicie
pozostawiony własnemu losowi, a następnie usunięty po wywołaniu destruktora.
Pierwszy środek zapobiegawczy — więcej przeciążania
Są dwa sposoby, by poprawić efektywność działania takiego poddanego przeciążeniu ope-
ratora przypisania. Zmiana typu parametru funkcji operatorowej z obiektu klasy na
macierz znakową (łańcuch znaków) pozwala na wyeliminowanie wywołania konstruktora
konwersji.
; ; "" # => ! (&
! & & &(.
#
# 0E &( &
R" H+ +HJ+ ) &
7 ,
Jeśli chcemy, by poddany przeciążeniu wobec klasy operator przypisania obsługi-
wał i przypisanie obiektowi tablicy znakowej, i przypisanie obiektu klasy , musimy