Download as pdf or txt
Download as pdf or txt
You are on page 1of 59

IDZ DO

PRZYKADOWY ROZDZIA
SPIS TRECI

KATALOG KSIEK
KATALOG ONLINE
ZAMW DRUKOWANY KATALOG

TWJ KOSZYK
DODAJ DO KOSZYKA

CENNIK I INFORMACJE
ZAMW INFORMACJE
O NOWOCIACH
ZAMW CENNIK

CZYTELNIA
FRAGMENTY KSIEK ONLINE

C++. 50 efektywnych
sposobw na udoskonalenie
Twoich programw
Autor: Scott Meyers
Tumaczenie: Mikoaj Szczepaniak
ISBN: 83-7361-345-5
Tytu oryginau: Effective C++: 50 Specific Ways
to Improve Your Programs and Design
Format: B5, stron: 248
Pierwsze wydanie ksiki C++. 50 efektywnych sposobw na udoskonalenie twoich
programw zostao sprzedane w nakadzie 100 000 egzemplarzy i zostao
przetumaczone na cztery jzyki. Nietrudno zrozumie, dlaczego tak si stao.
Scott Meyers w charakterystyczny dla siebie, praktyczny sposb przedstawi wiedz
typow dla ekspertw czynnoci, ktre niemal zawsze wykonuj lub czynnoci,
ktrych niemal zawsze unikaj, by tworzy prosty, poprawny i efektywny kod.
Kada z zawartych w tej ksice pidziesiciu wskazwek jest streszczeniem metod
pisania lepszych programw w C++, za odpowiednie rozwaania s poparte
konkretnymi przykadami. Z myl o nowym wydaniu, Scott Meyers opracowa
od pocztku wszystkie opisywane w tej ksice wskazwki. Wynik jego pracy jest
wyjtkowo zgodny z midzynarodowym standardem C++, technologi aktualnych
kompilatorw oraz najnowszymi trendami w wiecie rzeczywistych aplikacji C++.
Do najwaniejszych zalet ksiki C++. 50 efektywnych sposobw na udoskonalenie
twoich programw nale:
Eksperckie porady dotyczce projektowania zorientowanego obiektowo,
projektowania klas i waciwego stosowania technik dziedziczenia
Analiza standardowej biblioteki C++, wcznie z wpywem standardowej biblioteki
szablonw oraz klas podobnych do string i vector na struktur dobrze napisanych
programw
Rozwaania na temat najnowszych moliwoci jzyka C++: inicjalizacji staych
wewntrz klas, przestrzeni nazw oraz szablonw skadowych
Wiedza bdca zwykle w posiadaniu wycznie dowiadczonych programistw
Ksika C++. 50 efektywnych sposobw na udoskonalenie twoich programw
pozostaje jedn z najwaniejszych publikacji dla kadego programisty pracujcego z C++.

Wydawnictwo Helion
ul. Chopina 6
44-100 Gliwice
tel. (32)230-98-63
e-mail: helion@helion.pl

Scott Meyers jest znanym autorytetem w dziedzinie programowania w jzyku C++;


zapewnia usugi doradcze dla klientw na caym wiecie i jest czonkiem rady
redakcyjnej pisma C++ Report. Regularnie przemawia na technicznych konferencjach na
caym wiecie, jest take autorem ksiek More Effective C++ oraz Effective C++ CD.
W 1993. roku otrzyma tytu doktora informatyki na Brown University.

Spis treci
Przedmowa ................................................................................................................... 7
Podzikowania ............................................................................................................ 11
Wstp......................................................................................................................... 15
Przejcie od jzyka C do C++....................................................................................... 27
Sposb 1. Wybieraj const i inline zamiast #define......................................................................28
Sposb 2. Wybieraj <iostream> zamiast <stdio.h>.....................................................................31
Sposb 3. Wybieraj new i delete zamiast malloc i free...............................................................33
Sposb 4. Stosuj komentarze w stylu C++ ..................................................................................34
Zarzdzanie pamici .................................................................................................. 37
Sposb 5. Uywaj tych samych form w odpowiadajcych sobie zastosowaniach
operatorw new i delete ..............................................................................................38
Sposb 6. Uywaj delete w destruktorach dla skadowych wskanikowych ..............................39
Sposb 7. Przygotuj si do dziaania w warunkach braku pamici.............................................40
Sposb 8. Podczas pisania operatorw new i delete trzymaj si istniejcej konwencji ..............48
Sposb 9. Unikaj ukrywania normalnej formy operatora new ................................................51
Sposb 10. Jeli stworzye wasny operator new, opracuj take wasny operator delete ............53
Konstruktory, destruktory i operatory przypisania ......................................................... 61
Sposb 11. Deklaruj konstruktor kopiujcy i operator przypisania dla klas
z pamici przydzielan dynamicznie ........................................................................61
Sposb 12. Wykorzystuj konstruktory do inicjalizacji, a nie przypisywania wartoci .................64
Sposb 13. Umieszczaj skadowe na licie inicjalizacji w kolejnoci zgodnej
z kolejnoci ich deklaracji.........................................................................................69
Sposb 14. Umieszczaj w klasach bazowych wirtualne destruktory ............................................71
Sposb 15. Funkcja operator= powinna zwraca referencj do *this ...........................................76
Sposb 16. Wykorzystuj operator= do przypisywania wartoci do wszystkich skadowych klasy......79
Sposb 17. Sprawdzaj w operatorze przypisania, czy nie przypisujesz wartoci samej sobie......82
Klasy i funkcje projekt i deklaracja.......................................................................... 87
Sposb 18. Staraj si dy do kompletnych i minimalnych interfejsw klas..............................89
Sposb 19. Rozrniaj funkcje skadowe klasy, funkcje niebdce skadowymi
klasy i funkcje zaprzyjanione....................................................................................93

Spis treci
Sposb 20. Unikaj deklarowania w interfejsie publicznym skadowych reprezentujcych dane ........98
Sposb 21. Wykorzystuj stae wszdzie tam, gdzie jest to moliwe...........................................100
Sposb 22. Stosuj przekazywanie obiektw przez referencje, a nie przez wartoci ...................106
Sposb 23. Nie prbuj zwraca referencji, kiedy musisz zwrci obiekt ...................................109
Sposb 24. Wybieraj ostronie pomidzy przecianiem funkcji
a domylnymi wartociami parametrw ...................................................................113
Sposb 25. Unikaj przeciania funkcji dla wskanikw i typw numerycznych......................117
Sposb 26. Strze si niejednoznacznoci...................................................................................120
Sposb 27. Jawnie zabraniaj wykorzystywania niejawnie generowanych funkcji
skadowych, ktrych stosowanie jest niezgodne z Twoimi zaoeniami..................123
Sposb 28. Dziel globaln przestrze nazw ................................................................................124
Implementacja klas i funkcji ...................................................................................... 131
Sposb 29. Unikaj zwracania uchwytw do wewntrznych danych .......................................132
Sposb 30. Unikaj funkcji skadowych zwracajcych zmienne wskaniki
lub referencje do skadowych, ktre s mniej dostpne od tych funkcji ..................136
Sposb 31. Nigdy nie zwracaj referencji do obiektu lokalnego ani do wskanika
zainicjalizowanego za pomoc operatora new wewntrz tej samej funkcji .............139
Sposb 32. Odkadaj definicje zmiennych tak dugo, jak to tylko moliwe ...............................142
Sposb 33. Rozwanie stosuj atrybut inline ................................................................................144
Sposb 34. Ograniczaj do minimum zalenoci czasu kompilacji midzy plikami....................150
Dziedziczenie i projektowanie zorientowane obiektowo ............................................... 159
Sposb 35. Dopilnuj, by publiczne dziedziczenie modelowao relacj jest ............................160
Sposb 36. Odrniaj dziedziczenie interfejsu od dziedziczenia implementacji ........................166
Sposb 37. Nigdy nie definiuj ponownie dziedziczonych funkcji niewirtualnych .....................174
Sposb 38. Nigdy nie definiuj ponownie dziedziczonej domylnej wartoci parametru ............176
Sposb 39. Unikaj rzutowania w d hierarchii dziedziczenia....................................................178
Sposb 40. Modelujc relacje posiadania (ma) i implementacji z wykorzystaniem,
stosuj podzia na warstwy .........................................................................................186
Sposb 41. Rozrniaj dziedziczenie od stosowania szablonw ................................................189
Sposb 42. Dziedziczenie prywatne stosuj ostronie ..................................................................193
Sposb 43. Dziedziczenie wielobazowe stosuj ostronie............................................................199
Sposb 44. Mw to, o co czym naprawd mylisz. Zdawaj sobie spraw z tego, co mwisz ....213
Rozmaitoci .............................................................................................................. 215
Sposb 45. Miej wiadomo, ktre funkcje s niejawnie tworzone i wywoywane przez C++....... 215
Sposb 46. Wykrywanie bdw kompilacji i czenia jest lepsze
od wykrywania bdw podczas wykonywania programw ....................................219
Sposb 47. Upewnij si, e nielokalne obiekty statyczne s inicjalizowane przed ich uyciem .......222
Sposb 48. Zwracaj uwag na ostrzeenia kompilatorw...........................................................226
Sposb 49. Zapoznaj si ze standardow bibliotek C++ ...........................................................227
Sposb 50. Pracuj bez przerwy nad swoj znajomoci C++ .....................................................234
Skorowidz ................................................................................................................. 239

Dziedziczenie
i projektowanie
zorientowane obiektowo
Wielu programistw wyraa opini, e moliwo dziedziczenia jest jedyn korzyci
pync z programowania zorientowanego obiektowo. Mona mie oczywicie rne
zdanie na ten temat, jednak liczba zawartych w innych czciach tej ksiki sposobw
powiconych efektywnemu programowaniu w C++ pokazuje, e masz do dyspozycji
znacznie wicej rozmaitych narzdzi, ni tylko okrelanie, ktre klasy powinny dziedziczy po innych klasach.
Projektowanie i implementowanie hierarchii klas rni si od zasadniczo od wszystkich mechanizmw dostpnych w jzyku C. Problem dziedziczenia i projektowania
zorientowanego obiektowo z pewnoci zmusza do ponownego przemylenia swojej
strategii konstruowania systemw oprogramowania. Co wicej, jzyk C++ udostpnia
bardzo szeroki asortyment blokw budowania obiektw, wcznie z publicznymi,
chronionymi i prywatnymi klasami bazowymi, wirtualnymi i niewirtualnymi klasami
bazowymi oraz wirtualnymi i niewirtualnymi funkcjami skadowymi. Kada z wymienionych wasnoci moe wpywa take na pozostae komponenty jzyka C++.
W efekcie, prby zrozumienia, co poszczeglne wasnoci oznaczaj, kiedy powinny
by stosowane oraz jak mona je w najlepszy sposb poczy z nieobiektowymi czciami jzyka C++ moe niedowiadczonych programistw zniechci.
Dalsz komplikacj jest fakt, e rne wasnoci jzyka C++ s z pozoru odpowiedzialne za te same zachowania. Oto przykady:
 Potrzebujesz zbioru klas zawierajcych wiele elementw wsplnych.

Powiniene wykorzysta mechanizm dziedziczenia i stworzy klasy


potomne wzgldem jednej wsplnej klasy bazowej czy powiniene
wykorzysta szablony i wygenerowa wszystkie potrzebne klasy
ze wsplnym szkieletem kodu?

160

Dziedziczenie i projektowanie zorientowane obiektowo


 Klasa A ma zosta zaimplementowana w oparciu o klas B. Czy A powinna

zawiera skadow reprezentujc obiekt klasy B czy te powinna prywatnie


dziedziczy po klasie B?
 Potrzebujesz projektu bezpiecznej pod wzgldem typu i homogenicznej klasy

pojemnikowej, ktra nie jest dostpna w standardowej bibliotece C++ (list


pojemnikw udostpnianych przez t bibliotek podano, prezentujc sposb 49.).
Czy lepszym rozwizaniem bdzie skonstruowanie szablonw czy budowa
bezpiecznych pod wzgldem typw interfejsw wok tej klasy, ktra sama
byaby zaimplementowana za pomoc oglnych () wskanikw?
W sposobach prezentowanych w tej czci zawarem wskazwki, jak naley znajdowa odpowiedzi na powysze pytania. Nie mog jednak liczy na to, e uda mi si
znale waciwe rozwizania dla wszystkich aspektw projektowania zorientowanego obiektowo. Zamiast tego skoncentrowaem si wic na wyjanianiu, co faktycznie
oznaczaj poszczeglne wasnoci jzyka C++ i co tak naprawd sygnalizujesz, stosujc poszczeglne dyrektywy czy instrukcje. Przykadowo, publiczne dziedziczenie
oznacza relacj jest lub specjalizacji-generalizacji (ang. isa, patrz sposb 35.) i jeli
musisz nada mu jakikolwiek inny sens, moesz napotka pewne problemy. Podobnie,
funkcje wirtualne oznaczaj, e interfejs musi by dziedziczony, natomiast funkcje
niewirtualne oznaczaj, e dziedziczony musi by zarwno interfejs, jak i implementacja. Brak rozrnienia tych znacze doprowadzi ju wielu programistw C++
do trudnych do opisania nieszcz.
Jeli rozumiesz znaczenia rozmaitych wasnoci jzyka C++, odkryjesz, e Twj pogld na projektowanie zorientowane obiektowo powoli ewoluuje. Zamiast przekonywa
Ci o istniejcych rnicach pomidzy konstrukcjami jzykowymi, tre poniszych
sposobw uatwi Ci ocen jakoci opracowanych dotychczas systemw oprogramowania. Bdziesz potem w stanie przeksztaci swoj wiedz w swobodne i waciwe
operowanie wasnociami jzyka C++ celem tworzenia jeszcze lepszych programw.
Wartoci wiedzy na temat znacze i konsekwencji stosowania poszczeglnych konstrukcji nie da si przeceni. Ponisze sposoby zawieraj szczegow analiz metod
efektywnego stosowania omawianych wasnoci jzyka C++. W sposobie 44. podsumowaem cechy i znaczenia poszczeglnych konstrukcji obiektowych tego jzyka.
Tre tego sposobu naley traktowa jak zwieczenie caej czci, a take zwize
streszczenie, do ktrego warto zaglda w przyszoci.

Sposb 35.
Dopilnuj, by publiczne dziedziczenie
modelowao relacj jest
Sposb 35. Dopilnuj, by publiczne dziedziczenie modelowao relacj jest

William Dement w swojej ksice pt. Some Must Watch While Some Must Sleep
(W. H. Freeman and Company, 1974) opisa swoje dowiadczenia z pracy ze studentami, kiedy prbowa utrwali w ich umysach najistotniejsze tezy swojego wykadu.

Sposb 35. Dopilnuj, by publiczne dziedziczenie modelowao relacj jest

161

Mwi im, e przyjmuje si, e wiadomo historyczna przecitnego brytyjskiego


dziecka w wieku szkolnym wykracza poza wiedz, e bitwa pod Hastings odbya si
w roku 1066. William Dement podkrela, e jeli dziecko pamita wicej szczegw,
musi take pamita o tej historycznej dla Brytyjczykw dacie. Na tej podstawie autor
wnioskuje, e w umysach jego studentw zachowuje si tylko kilka istotnych i najciekawszych faktw, wcznie z tym, e np. tabletki nasenne powoduj bezsenno.
Namawia studentw, by zapamitali przynajmniej tych kilka najwaniejszych
faktw, nawet jeli maj zapomnie wszystkie pozostae zagadnienia dyskutowane
podczas wykadw. Autor ksiki przekonywa do tego swoich studentw wielokrotnie w czasie semestru.
Ostatnie pytanie testu w sesji egzaminacyjnej brzmiao: wymie jeden fakt, ktry wyniose z moich wykadw, i ktry na pewno zapamitasz do koca ycia. Po sprawdzeniu egzaminw Dement by zszokowany niemal wszyscy napisali 1066.
Jestem teraz peen obaw, e jedynym istotnym wnioskiem, ktry wyniesiesz z tej
ksiki na temat programowania zorientowanego obiektowo w C++ bdzie to, e mechanizm publicznego dziedziczenia oznacza relacj jest. Zachowaj jednak ten fakt
w swojej pamici.
Jeli piszesz klas D (od ang. Derived, czyli klas potomn), ktra publicznie dziedziczy po klasie B (od ang. Base, czyli klasy bazowej), sygnalizujesz kompilatorom C++
(i przyszym czytelnikom Twojego kodu), e kady obiekt typu D jest take obiektem
typu B, ale nie odwrotnie. Sygnalizujesz, e B reprezentuje bardziej oglne pojcia
ni D, natomiast D reprezentuje bardziej konkretne pojcia ni B. Utrzymujesz, e
wszdzie tam, gdzie moe by uyty obiekt typu B, moe by take wykorzystany
obiekt typu D, poniewa kady obiekt typu D jest take obiektem typu B. Z drugiej
strony, jeli potrzebujesz obiektu typu D, obiekt typu B nie bdzie mg go zastpi
publiczne dziedziczenie oznacza relacj D jest B, ale nie odwrotnie.
Tak interpretacj publicznego dziedziczenia wymusza jzyk C++. Przeanalizujmy
poniszy przykad:

   


   

Oczywiste jest, e kady student jest osob, nie kada osoba jest jednak studentem.
Dokadnie takie samo znaczenie ma powysza hierarchia. Oczekujemy, e wszystkie
istniejce cechy danej osoby (np. to, e ma jak dat urodzenia) istniej take dla studenta; nie oczekujemy jednak, e wszystkie dane dotyczce studenta (np. adres szkoy,
do ktrej uczszcza) bd istotne dla wszystkich ludzi. Pojcie osoby jest bowiem
bardziej oglne, ni pojcie studenta student jest specyficznym rodzajem osoby.
W jzyku C++ kada funkcja oczekujca argumentu typu  
(lub wskanika do
obiektu klasy  
bd referencji do obiektu klasy  
) moe zamiennie pobiera obiekt klasy 
(lub wskanik do obiektu klasy 
bd referencj do
obiektu klasy 
):
 


  !"
 

 
 #$ %"

162

Dziedziczenie i projektowanie zorientowane obiektowo



!
&  ' 


!
&
 


 !(!
&  '

 !(!
&
(
%'  '
 !
)$*
!
&


Powysze komentarze s prawdziwe tylko dla publicznego dziedziczenia. C++ bdzie


si zachowywa w opisany sposb tylko w przypadku, gdy klasa 
bdzie publicznie dziedziczya po klasie  
. Dziedziczenie prywatne oznacza co zupenie innego
(patrz sposb 42.), natomiast znaczenie dziedziczenia chronionego jest nieznane.
Rwnowano dziedziczenia publicznego i relacji jest wydaje si oczywista, w praktyce jednak waciwe modelowanie tej relacji nie jest ju takie proste. Niekiedy nasza
intuicja moe si okaza zawodna. Przykadowo, faktem jest, e pingwin to ptak;
faktem jest take, e ptaki mog lata. Gdybymy w swojej naiwnoci sprbowali wyrazi to w C++, nasze wysiki przyniosyby efekt podobny do poniszego:
+

 , #$"



#
+ 
#%
$



Mamy teraz problem, poniewa z powyszej hierarchii wynika, e pingwiny mog


lata, co jest oczywicie nieprawd. Co stao si z nasz strategi?
W tym przypadku padlimy ofiar nieprecyzyjnego jzyka naturalnego (polskiego).
Kiedy mwimy, e ptaki mog lata, w rzeczywistoci nie mamy na myli tego, e
wszystkie ptaki potrafi lata, a jedynie, e w oglnoci ptaki maj moliwo latania.
Gdybymy byli bardziej precyzyjni, wyrazilibymy si inaczej, by podkreli fakt, e
istnieje wiele gatunkw ptakw, ktre nie lataj otrzymalibymy wwczas ponisz, znacznie lepiej modelujc rzeczywisto, hierarchi klas:
+
 &,
&,

-
#++

 ,


.
-
#++
 &,
&,


Sposb 35. Dopilnuj, by publiczne dziedziczenie modelowao relacj jest

163


#
.
-
#+
 &,
&,


Powysza hierarchia jest znacznie blisza naszej rzeczywistej wiedzy na temat ptakw, ni ta zaprezentowana wczeniej.
Nasze rozwizanie nie jest jednak jeszcze skoczone, poniewa w niektrych systemach oprogramowania, proste stwierdzenie, e pingwin jest ptakiem, bdzie cakowicie
poprawne. W szczeglnoci, jeli nasza aplikacja dotyczy wycznie dziobw i skrzyde,
a w adnym stopniu nie wie si z lataniem, oryginalna hierarchia bdzie w zupenoci wystarczajca. Mimo e jest to dosy irytujce, omawiana sytuacja jest prostym
odzwierciedleniem faktu, e nie istnieje jedna doskonaa metoda projektowania dowolnego oprogramowania. Dobry projekt musi po prostu uwzgldnia wymagania
stawiane przed tworzonym systemem, zarwno te w danej chwili oczywiste, jak i te,
ktre mog si pojawi w przyszoci. Jeli nasza aplikacja nie musi i nigdy nie bdzie
musiaa uwzgldnia moliwoci latania, rozwizaniem w zupenoci wystarczajcym
bdzie stworzenie klasy 
 
jako potomnej klasy . W rzeczywistoci taki
projekt moe by nawet lepszy ni rozrnienie ptakw latajcych od nielatajcych,
poniewa takie rozrnienie moe w ogle nie istnie w modelowanym wiecie.
Dodawanie do hierarchii niepotrzebnych klas jest bdn decyzj projektow, poniewa narusza prawidowe relacje dziedziczenia pomidzy klasami.
Istnieje jeszcze inna strategia postpowania w przypadku omawianego problemu:
wszystkie ptaki mog lata, pingwiny s ptakami, pingwiny nie mog lata. Strategia polega na wprowadzeniu takich zmian w definicji funkcji , by dla pingwinw
generowany by bd wykonania:
  

##,
&!,
 %
%

&

#
+

 ,  /
#%

 #$"*/



Do takiego rozwizania d twrcy jzykw interpretowanych (jak Smalltalk), jednak istotne jest prawidowe rozpoznanie rzeczywistego znaczenia powyszego kodu,
ktre jest zupenie inne, ni mgby przypuszcza. Ciao funkcji nie oznacza bowiem,
e pingwiny nie mog lata. Jej faktyczne znaczenie to: pingwiny mog lata, jednak kiedy prbuj to robi, powoduj bd. Na czym polega rnica pomidzy tymi
znaczeniami? Wynika przede wszystkim z moliwoci wykrycia bdu ograniczenie pingwiny nie mog lata moe by egzekwowane przez kompilatory, natomiast
naruszenie ograniczenia podejmowana przez pingwiny prba latania powoduje bd
moe zosta wykryte tylko podczas wykonywania programu.
Aby wyrazi ograniczenie pingwiny nie mog lata, wystarczy nie definiowa
odpowiedniej funkcji dla obiektw klasy 
 
:
+
 &,
&,


164

Dziedziczenie i projektowanie zorientowane obiektowo


.
-
#++
 &,
&,


#
.
-
#+
 &,
&,


Jeli sprbujesz teraz wywoa funkcj  dla obiektu reprezentujcego pingwina,
kompilator zasygnalizuje bd:

#

 ,)$*

Zaprezentowane rozwizanie jest cakowicie odmienne od podejcia stosowanego


w jzyku Smalltalk. Stosowana tam strategia powoduje, e kompilator skompilowaby
podobny kod bez przeszkd.
Filozofia jzyka C++ jest jednak zupenie inna ni filozofia jzyka Smalltalk, dopki
jednak programujesz w C++, powiniene stosowa si wycznie do regu obowizujcych w tym jzyku. Co wicej, wykrywanie bdw w czasie kompilacji (a nie
w czasie wykonywania) programu wie si z pewnymi technicznymi korzyciami
patrz sposb 46.
By moe przyznasz, e Twoja wiedza z zakresu ornitologii ma raczej intuicyjny
charakter i moe by zawodna, zawsze moesz jednak polega na swojej biegoci
w dziedzinie podstawowej geometrii, prawda? Nie martw si, mam na myli wycznie prostokty i kwadraty.
Sprbuj wic odpowiedzie na pytanie: czy reprezentujca kwadraty klasa  
publicznie dziedziczy po reprezentujcej prostokty klasie  
?

Powiesz pewnie: Te co! Kade dziecko wie, e kwadrat jest prostoktem, ale w oglnoci prostokt nie musi by kwadratem. Tak, to prawda, przynajmniej na poziomie
gimnazjum. Nie sdz jednak, bymy kiedykolwiek wrcili do nauki na tym poziomie.
Przeanalizuj wic poniszy kod:
0
#

 1#2

%1#2
 32

%32

2#2
,
&!%&$$

%2
% 4



Sposb 35. Dopilnuj, by publiczne dziedziczenie modelowao relacj jest

165

 +##0
#,
&!%'!&$ 
 %!2
  $

 1#25 2#2
 32 %2678 &78 !  4
 2#255 1#2%
'(%  4"
  $
!
)'

Jest oczywiste, e ostatnia instrukcja nigdy nie zakoczy si niepowodzeniem,


poniewa funkcja  modyfikuje wycznie szeroko prostokta reprezentowanego przez .
Rozwa teraz poniszy fragment kodu, w ktrym wykorzystujemy publiczne dziedziczenie umoliwiajce traktowanie kwadratw jak prostoktw:
90
#   
9

 %255 2#2%
"%!%
%!2%:%
+##
!!!
(
&%&/&/!$
0
#( %'!%'!"
 &#  %!2

 3255 2#2%

"%!%
%!2%:%

Take teraz oczywiste jest, e ostatni warunek nigdy nie powinien by faszywy.
Zgodnie z definicj, szeroko kwadratu jest przecie taka sama jak jego wysoko.
Tym razem mamy jednak problem. Jak mona pogodzi ponisze twierdzenia?
 Przed wywoaniem funkcji  wysoko kwadratu reprezentowanego
przez obiekt jest taka sama jak jego szeroko.
 Wewntrz funkcji  modyfikowana jest szeroko kwadratu,

jednak wysoko pozostaje niezmieniona.


 Po zakoczeniu wykonywania funkcji  wysoko kwadratu
ponownie jest taka sama jak jego szeroko (zauwa, e obiekt jest
przekazywany do funkcji  przez referencj, zatem funkcja
modyfikuje ten sam obiekt , nie jego kopi).

Jak to moliwe?
Witaj w cudownym wiecie publicznego dziedziczenia, w ktrym Twj instynkt
sprawdzajcy si do tej pory w innych dziedzinach, wcznie z matematyk moe
nie by tak pomocny, jak tego oczekujesz. Zasadniczym problemem jest w tym przypadku to, e operacja, ktr mona stosowa dla prostoktw (jego szeroko moe
by zmieniana niezalenie od wysokoci), nie moe by stosowana dla kwadratw
(definicja figury wymusza rwno jej szerokoci i wysokoci). Mechanizm publicznego dziedziczenia zakada jednak, e absolutnie wszystkie operacje stosowane z powodzeniem dla obiektw klasy bazowej mog by stosowane take dla obiektw klasy

166

Dziedziczenie i projektowanie zorientowane obiektowo

potomnej. W przypadku prostoktw i kwadratw (podobny przykad dotyczcy zbiorw i list omawiam w sposobie 40.) to zaoenie si nie sprawdza, zatem stosowanie
publicznego dziedziczenia do modelowania wystpujcej midzy nimi relacji jest po
prostu bdne. Kompilatory oczywicie umoliwi Ci zaprogramowanie takiego modelu, jednak jak si ju przekonalimy nie mamy gwarancji, e nasz program
bdzie si zachowywa prawidowo. Od czasu do czasu kady programista musi si
przekona (niektrzy czciej, inni rzadziej), e poprawne skompilowanie programu
nie oznacza, e bdzie on dziaa zgodnie z oczekiwaniami.
Nie denerwuj si, e rozwijana przez lata intuicja dotyczca tworzonego oprogramowania traci moc w konfrontacji z projektowaniem zorientowanym obiektowo. Twoja
wiedza jest nadal cenna, jednak dodae wanie do swojego arsenau rozwiza projektowych silny mechanizm dziedziczenia i bdziesz musia rozszerzy swoj intuicj
w taki sposb, by prowadzia Ci do waciwego wykorzystywania nowych umiejtnoci. Z czasem problem klasy 
 
dziedziczcej po klasie  lub klasie  
dziedziczcej po klasie  
 bdzie dla Ciebie rwnie zabawny jak prezentowane Ci przez niedowiadczonych programistw funkcje zajmujce wiele stron. Moliwe, e proponowane podejcie do tego typu problemw jest waciwe, nadal jednak
nie jest to bardzo prawdopodobne.
Relacja jest nie jest oczywicie jedyn relacj wystpujc pomidzy klasami. Dwie
pozostae powszechnie stosowane relacje midzy klasami to relacja ma (ang. has-a)
oraz relacja implementacji z wykorzystaniem (ang. is-implemented-in-terms-of).
Relacje te przeanalizujemy podczas prezentacji sposobw 40. i 42. Nierzadko projekty C++ ulegaj znieksztaceniom, poniewa ktra z pozostaych najwaniejszych
relacji zostaa bdnie zamodelowana jako jest, powinnimy wic by pewni, e
waciwie rozrniamy te relacje i wiemy, jak naley je najlepiej modelowa w C++.

Sposb 36.
Odrniaj dziedziczenie interfejsu
od dziedziczenia implementacji
Sposb 36. Odrniaj dziedziczenie interfejsu od dziedziczenia implementacji

Po przeprowadzeniu dokadnej analizy okazuje si, e pozornie oczywiste pojcie


(publicznego) dziedziczenia skada si w rzeczywistoci z dwch rozdzielnych czci
dziedziczenia interfejsw funkcji oraz dziedziczenia implementacji funkcji. Rnica
pomidzy wspomnianymi rodzajami dziedziczenia cile odpowiada rnicy pomidzy deklaracjami a definicjami funkcji (omwionej we wstpie do tej ksiki).
Jako projektant klasy potrzebujesz niekiedy takich klas potomnych, ktre dziedzicz
wycznie interfejs (deklaracj) danej funkcji skadowej; czasem potrzebujesz klas
potomnych dziedziczcych zarwno interfejs, jak i implementacj danej funkcji, jednak
masz zamiar przykry implementacj swoim rozwizaniem; zdarza si take, e
potrzebujesz klas potomnych dziedziczcych zarwno interfejs, jak i implementacj
danej funkcji, ale bez moliwoci przykrywania czegokolwiek.

Sposb 36. Odrniaj dziedziczenie interfejsu od dziedziczenia implementacji

167

Aby lepiej zrozumie rnic pomidzy zaproponowanymi opcjami, przeanalizuj


ponisz hierarchi klas reprezentujc figury geometryczne w aplikacji graficznej:
2

 %
58
  

##

 &;<



0
#2   
=2   

 jest klas abstrakcyjn. Mona to pozna po obecnoci czystej funkcji wirtualnej . W efekcie klienci nie mog tworzy egzemplarzy klasy , mog to robi
wycznie klasy potomne. Mimo to klasa  wywiera ogromny nacisk na wszystkie klasy, ktre (publicznie) po niej dziedzicz, poniewa:
 Interfejsy funkcji skadowych zawsze s dziedziczone. W sposobie 35.

wyjaniem, e dziedziczenie publiczne oznacza faktycznie relacj jest,


zatem wszystkie elementy istniejce w klasie bazowej musz take istnie
w klasach potomnych. Jeli wic dan funkcj mona wykona dla danej
klasy, musi take istnie sposb jej wykonania dla jej podklas.
W funkcji  zadeklarowalimy trzy funkcje. Pierwsza, , rysuje na ekranie
biecy obiekt. Druga, , jest wywoywana przez inne funkcje skadowe w momencie, gdy konieczne jest zasygnalizowanie bdu. Trzecia,  , zwraca unikalny
cakowitoliczbowy identyfikator biecego obiektu (przykad wykorzystania tego typu
funkcji znajdziesz w sposobie 17.). Kada z wymienionych funkcji zostaa zadeklarowana w inny sposb:  jest czyst funkcj wirtualn,  jest prost (nieczyst?)
funkcj wirtualn, natomiast   jest funkcj niewirtualn. Jakie jest znaczenie
tych trzech rnych deklaracji?
Rozwamy najpierw czyst funkcj wirtualn . Dwie najistotniejsze cechy czystych funkcji wirtualnych to konieczno ich ponownego zadeklarowania w kadej
dziedziczcej je konkretnej klasie oraz brak ich definicji w klasach abstrakcyjnych.
Jeli poczymy te wasnoci, uwiadomimy sobie, e:
 Celem deklarowania czystych funkcji wirtualnych jest otrzymanie klas

potomnych dziedziczcych wycznie interfejs.


Jest to idealne rozwizanie dla funkcji  , poniewa naturalne jest udostpnienie moliwoci rysowania wszystkich obiektw klasy , jednak niemoliwe
jest opracowanie jednej domylnej implementacji dla takiej funkcji. Algorytm rysowania np. elips rni si przecie znacznie od algorytmu rysowania prostoktw.
Waciwym sposobem interpretowania znaczenia deklaracji funkcji   jest
instrukcja skierowana do projektantw podklas: musicie stworzy funkcj , jednak nie mam pojcia, jak moglibycie j zaimplementowa.

168

Dziedziczenie i projektowanie zorientowane obiektowo

Istnieje niekiedy moliwo opracowania definicji czystej funkcji wirtualnej. Oznacza


to, e moesz stworzy tak implementacj dla funkcji  , e kompilatory
C++ nie zgosz adnych zastrzee, jednak jedynym sposobem jej wywoania byoby
wykorzystanie penej nazwy wcznie z nazw klasy:
2>5
%2)$*2&$&
$
2>75
%0
# !
7?@%%% )&0
#%
2>A5
%= !
A?@%%% )&=%
7?@2%%% )&2%
A?@2%%% )&2%

Poza faktem, e powysze rozwizanie moe zrobi wraenie na innych programistach podczas imprezy, w oglnoci znajomo zaprezentowanego fenomenu jest
w praktyce mao przydatna. Jak si jednak za chwil przekonasz, moe by wykorzystywana jako mechanizm udostpniania bezpieczniejszej domylnej implementacji dla
prostych (nieczystych) funkcji wirtualnych.
Niekiedy dobrym rozwizaniem jest zadeklarowanie klasy zawierajcej wycznie
czyste funkcje wirtualne. Takie klasy protokou udostpniaj klasom potomnym jedynie interfejsy funkcji, ale nigdy ich implementacje. Klasy protokou opisaem, prezentujc sposb 34., i wspominam o nich ponownie w sposobie 43.
Znaczenie prostych funkcji wirtualnych jest nieco inne ni znaczenie czystych funkcji
wirtualnych. W obu przypadkach klasy dziedzicz interfejsy funkcji, jednak proste
funkcje wirtualne zazwyczaj udostpniaj take swoje implementacje, ktre mog
(ale nie musz) by przykryte w klasach potomnych. Po chwili namysu powiniene
doj do wniosku, e:
 Celem deklarowania prostej funkcji wirtualnej jest otrzymanie klas potomnych

dziedziczcych zarwno interfejs, jak i domyln implementacj tej funkcji.


W przypadku funkcji   interfejs okrela, e kada klasa musi udostpnia
funkcj wywoywan w momencie wykrycia bdu, jednak obsuga samych bdw
jest dowolna i zaley wycznie od projektantw klas potomnych. Jeli nie przewiduj oni
adnych specjalnych dziaa w przypadku znalezienia bdu, mog wykorzysta udostpniany przez klas  domylny mechanizm obsugi bdw. Oznacza to, e rzeczywistym znaczeniem deklaracji funkcji   dla projektantw podklas jest zdanie: musisz obsuy funkcj , jednak jeli nie chcesz tworzy wasnej wersji
tej funkcji, moesz wykorzysta jej domyln wersj zdefiniowan dla klasy .
Okazuje si, e zezwalanie prostym funkcjom wirtualnym na precyzowanie zarwno
deklaracji, jak i domylnej implementacji moe by niebezpieczne. Aby przekona si
dlaczego, przeanalizuj zaprezentowan poniej hierarchi samolotw nalecych do
linii lotniczych XYZ. Linie XYZ posiadaj tylko dwa typy samolotw, Model A
i Model B, z ktrych oba lataj w identyczny sposb. Linie lotnicze XYZ zaprojektoway wic nastpujc hierarchi klas:

Sposb 36. Odrniaj dziedziczenie interfejsu od dziedziczenia implementacji

169

B    !


& 

B


 ,
B 




 B
,
B 



 
 





C BB
   
C +B
   

Aby wyrazi fakt, e wszystkie samoloty musz obsugiwa jak funkcj  oraz
z uwagi na moliwe wymagania dotyczce innych implementacji tej funkcji generowane przez nowe modele samolotw, funkcja !
  zostaa zadeklarowana
jako wirtualna. Aby unikn pisania identycznego kodu w klasach "! i ",
domylny model latania zosta jednak zapisany w formie ciaa funkcji !
 ,
ktre jest dziedziczone zarwno przez klas "!, jak i klas ".
Jest to klasyczny projekt zorientowany obiektowo. Dwie klasy wspdziel wsplny
element (sposb implementacji funkcji ), zatem element ten zostaje przeniesiony
do klasy bazowej i jest dziedziczony przez te dwie klasy. Takie rozwizanie ma wiele
istotnych zalet: pozwala unikn powielania tego samego kodu, uatwia przysze rozszerzenia systemu i upraszcza konserwacj w dugim okresie czasu wszystkie wymienione wasnoci s charakterystyczne wanie dla technologii obiektowej. Linie
lotnicze XYZ powinny wic by dumne ze swojego systemu.
Przypumy teraz, e firma XYZ rozwija si i postanowia pozyska nowy typ samolotu Model C. Nowy samolot rni si nieco od Modelu A i Modelu B, a w szczeglnoci ma inne waciwoci lotu.
Programici omawianych linii lotniczych dodaj wic do hierarchii klas reprezentujc samoloty Model C, jednak w popiechu zapomnieli ponownie zdefiniowa
funkcj :
C DB

 &,
&,


Ich kod zawiera wic co podobnego do poniszego fragmentu:


B E E'  
 %3!%
B
>5
%C D

?@,E%% )&,
&'B
,*

170

Dziedziczenie i projektowanie zorientowane obiektowo

Mamy do czynienia z prawdziw katastrof, a mianowicie z prb obsuenia lotu


obiektu klasy "#, jakby by obiektem klasy "! lub klasy ". Z pewnoci
nie wzbudzimy w ten sposb zaufania u klientw linii lotniczych.
Problem nie polega tutaj na tym, e zdefiniowalimy domylne zachowanie funkcji
!
 , tylko na tym, e klasa "# moga przypadkowo (na skutek nieuwagi
programistw) dziedziczy to zachowanie. Na szczcie istnieje moliwo przekazywania domylnych zachowa funkcji do podklas wycznie w przypadku, gdy ich twrcy
wyranie tego zadaj. Sztuczka polega na przerwaniu poczenia pomidzy interfejsem
wirtualnej funkcji a jej domyln implementacj. Oto sposb realizacji tego zadania:
B


 ,
B 

58

 
 ,-
B 



 B
,-
B 



 
 





Zwr uwag na sposb, w jaki przeksztacilimy !


  w czyst funkcj
wirtualn. Zaprezentowana klasa udostpnia tym samym interfejs funkcji obsugujcej latanie samolotw. Klasa !
 zawiera take jej domyln implementacj,
jednak tym razem w formie niezalenej funkcji,   $. Klasy podobne do "!
i " mog wykorzysta domyln implementacj, zwyczajnie wywoujc funkcj
  $ wbudowan w ciao ich funkcji  (jednak zanim to zrobisz, przeczytaj sposb 33., gdzie przeanalizowaem wzajemne oddziaywanie atrybutw 


i   dla funkcji skadowych):
C BB


 ,
B 


 ,-




C +B


 ,
B 


 ,-





W przypadku klasy "# nie moemy ju przypadkowo dziedziczy niepoprawnej


implementacji funkcji , poniewa czysta funkcja wirtualna w klasie !
 wymusza na projektantach nowej klasy stworzenie wasnej wersji funkcji :

Sposb 36. Odrniaj dziedziczenie interfejsu od dziedziczenia implementacji

171

C DB


 ,
B 




 C D,
B 



 
 
  




Powyszy schemat postpowania nie jest oczywicie cakowicie bezpieczny (programici nadal maj moliwo popeniania fatalnych w skutkach bdw), jednak jest
znacznie bardziej niezawodny od oryginalnego projektu. Funkcja !
   $
jest chroniona, poniewa w rzeczywistoci jest szczegem implementacyjnym klasy
!
 i jej klas potomnych. Klienci wykorzystujcy te klasy powinni zajmowa si
wycznie wasnociami lotu reprezentowanych samolotw, a nie sposobami implementowania tych wasnoci.
Wane jest take to, e funkcja !
   $ jest niewirtualna. Wynika to
z faktu, e adna z podklas nie powinna jej ponownie definiowa temu zagadnieniu
powiciem sposb 37. Gdyby funkcja   $ bya wirtualna, mielibymy do
czynienia ze znanym nam ju problemem: co stanie si, jeli projektant ktrej z podklas zapomni ponownie zdefiniowa funkcj   $ w sytuacji, gdzie bdzie to
konieczne?
Niektrzy programici sprzeciwiaj si idei definiowania dwch osobnych funkcji dla
interfejsu i domylnej implementacji (jak  i   $). Z jednej strony zauwaaj, e takie rozwizanie zamieca przestrze nazw klasy wystpujcymi wielokrotnie
zblionymi do siebie nazwami funkcji. Z drugiej strony zgadzaj si z tez, e naley
oddzieli interfejs od domylnej implementacji. Jak wic powinnimy radzi sobie
z t pozorn sprzecznoci? Wystarczy wykorzysta fakt, e czyste funkcje wirtualne
musz by ponownie deklarowane w podklasach, ale mog take zawiera wasne
implementacje. Oto sposb, w jaki moemy wykorzysta w hierarchii klas reprezentujcych samoloty moliwo definiowania czystej funkcji wirtualnej:
B


 ,
B 

58


 B
,
B 



 
 





C BB


 ,
B 


 B
,





172

Dziedziczenie i projektowanie zorientowane obiektowo


C +B


 ,
B 


 B
,




C DB


 ,
B 




 C D,
B 



 
 
  




Powyszy schemat niemal nie rni si od wczeniejszego projektu z wyjtkiem ciaa


czystej funkcji wirtualnej !
 , ktra zastpia wykorzystywan wczeniej
niezalen funkcj !
   $. W rzeczywistoci funkcja  zostaa rozbita na dwa najwaniejsze elementy. Pierwszym z nich jest deklaracja okrelajca jej
interfejs (ktry musi by wykorzystywany przez klasy potomne), natomiast drugim
jest definicja okrelajca domylne zachowanie funkcji (ktra moe by wykorzystana
w klasach domylnych, ale tylko na wyrane danie ich projektantw). czc funkcje  i   $, stracilimy jednak moliwo nadawania im rnych ogranicze
dostpu kod, ktry wczeniej by chroniony (funkcja   $ bya zadeklarowana w bloku   ) bdzie teraz publiczny (poniewa znajduje si w zadeklarowanej w bloku   funkcji ).
Wrmy do nalecej do klasy  niewirtualnej funkcji  . Kiedy funkcja
skadowa jest niewirtualna, w zamierzeniu nie powinna zachowywa si w klasach
potomnych inaczej ni w klasie bazowej. W rzeczywistoci niewirtualne funkcje
skadowe opisuj zachowanie niezalene od specjalizacji, poniewa implementacja
funkcji nie powinna ulega adnym zmianom, niezalenie od specjalizacji kolejnych
poziomw w hierarchii klas potomnych. Oto pyncy z tego wniosek:
 Celem deklarowania niewirtualnej funkcji jest otrzymanie klas potomnych

dziedziczcych zarwno interfejs, jak i wymagan implementacj tej funkcji.


Moesz pomyle, e deklaracja funkcji    oznacza: kady obiekt klasy
 zawiera funkcj zwracajc identyfikator obiektu, ktry zawsze jest wyznaczany
w ten sam sposb (opisany w definicji funkcji   ), ktrego adna klasa
potomna nie powinna prbowa modyfikowa. Poniewa niewirtualna funkcja opisuje zachowanie niezalene od specjalizacji, nigdy nie powinna by ponownie deklarowana w adnej podklasie (to zagadnienie szczegowo omwiem w sposobie 37.).
Rnice pomidzy deklaracjami czystych funkcji wirtualnych, prostych funkcji wirtualnych oraz funkcji niewirtualnych umoliwiaj dokadne precyzowanie waciwych
dla danego mechanizmw dziedziczenia tych funkcji przez klasy potomne: dziedziczenia samego interfejsu, dziedziczenia interfejsu i domylnej implementacji lub
dziedziczenia interfejsu i wymaganej implementacji. Poniewa wymienione rne

Sposb 36. Odrniaj dziedziczenie interfejsu od dziedziczenia implementacji

173

typy deklaracji oznaczaj zupenie inne mechanizmy dziedziczenia, podczas deklarowania funkcji skadowych musisz bardzo ostronie wybra jedn z omawianych metod.
Pierwszym popularnym bdem jest deklarowanie wszystkich funkcji jako niewirtualnych. Eliminujemy w ten sposb moliwo specjalizacji klas potomnych; szczeglnie kopotliwe s w tym przypadku take niewirtualne destruktory (patrz sposb 14.).
Jest to oczywicie dobre rozwizanie dla klas, ktre w zaoeniu nie bd wykorzystywane w charakterze klas bazowych. W takim przypadku, zastosowanie zbioru wycznie niewirtualnych funkcji skadowych jest cakowicie poprawne. Zbyt czsto
jednak wynika to wycznie z braku wiedzy na temat rni pomidzy funkcjami
wirtualnymi a niewirtualnymi lub nieuzasadnionych obaw odnonie wydajnoci funkcji
wirtualnych. Naley wic pamita o fakcie, e niemal wszystkie klasy, ktre w przyszoci maj by wykorzystane jako klasy bazowe, powinny zawiera funkcje wirtualne (ponownie patrz sposb 14.).
Jeli obawiasz si kosztw zwizanych z funkcjami wirtualnymi, pozwl, e przypomn Ci o regule 80-20 (patrz take sposb 33.), ktra mwi, e 80 procent czasu
dziaania programu jest powicona wykonywaniu 20 procent jego kodu. Wspomniana regua jest istotna, poniewa oznacza, e rednio 80 procent naszych wywoa
funkcji moe by wirtualnych i bdzie to miao niemal niezauwaalny wpyw na cakowit wydajno naszego programu. Zanim wic zaczniesz si martwi, czy moesz
sobie pozwoli na koszty zwizane z wykorzystaniem funkcji wirtualnych, upewnij
si, czy Twoje rozwaania dotycz tych 20 procent programu, gdzie decyzja bdzie
miaa istotny wpyw na wydajno caego programu.
Innym powszechnym problemem jest deklarowanie wszystkich funkcji jako wirtualne.
Niekiedy jest to oczywicie waciwe rozwizanie np. w przypadku klas protokou
(patrz sposb 34.). Moe jednak wiadczy take o zwykej niewiedzy projektanta
klasy. Niektre deklaracje funkcji nie powinny umoliwia ponownego ich definiowania w klasach potomnych w takich przypadkach jedynym sposobem osignicia
tego celu jest deklarowanie tych funkcji jako niewirtualnych. Nie ma przecie najmniejszego sensu udostpnianie innym programistom klas, ktre maj by dziedziczone przez inne klasy i ktrych wszystkie funkcje skadowe bd ponownie definiowane. Pamitaj, e jeli masz klas bazow , klas potomn  oraz funkcj skadow
, wwczas kade z poniszych wywoa funkcji  musi by prawidowe:
<>5
%<
>5
?@,%% )&,
&',!  $
%F
 ! %&
?@,%% )&,
&',!  $
%F
   
&

Niekiedy musisz zadeklarowa funkcj  jako niewirtualn, by upewni si, e


wszystko bdzie dziaao zgodnie z Twoimi oczekiwaniami (patrz sposb 37.). Jeli
dziaanie funkcji powinno by niezalene od specjalizacji, nie obawiaj si takiego
rozwizania.

174

Dziedziczenie i projektowanie zorientowane obiektowo

Sposb 37.
Nigdy nie definiuj ponownie dziedziczonych
funkcji niewirtualnych
Sposb 37. Nigdy nie definiuj ponownie dziedziczonych funkcji niewirtualnych

Istniej dwa podejcia do tego problemu: teoretyczne i pragmatyczne. Zacznijmy od podejcia pragmatycznego (teoretycy s w kocu przyzwyczajeni do cierpliwego czekania).
Przypumy, e powiem Ci, e klasa  publicznie dziedziczy po klasie  i istnieje
publiczna funkcja skadowa  zdefiniowana w klasie . Parametry i warto zwracane przez funkcj  s dla nas na tym etapie nieistotne, zamy wic, e maj posta
. Innymi sowy, moemy to wyrazi w nastpujcy sposb:
+

 ,


<+   

Nawet gdybymy nic nie wiedzieli o ,  i , majc dany obiekt % klasy :
<GG& <

bylibymy bardzo zaskoczeni, gdyby instrukcje:


+>+5G !&%F
 G
+?@,%% )&,
&',!  $%F


powodoway inne dziaanie, ni instrukcje:


<><5G !&%F
 G
<?@,%% )&,
&',!  $%F


Wynika to z faktu, e w obu przypadkach wywoujemy funkcj skadow  dla


obiektu %. Poniewa w obu przypadkach jest to ta sama funkcja i ten sam obiekt, efekt
wywoania powinien by identyczny, prawda?
Tak, powinien, ale nie jest. W szczeglnoci, rezultaty wywoania bd inne, jeli 
bdzie funkcj niewirtualn, a klasa  bdzie zawieraa definicj wasnej wersji tej
funkcji:
<+

 ,%,
&'+,! :H8


+?@,%% )&,
&'+,
<?@,%% )&,
&'<,

Sposb 37. Nigdy nie definiuj ponownie dziedziczonych funkcji niewirtualnych

175

Powodem takiego dwulicowego zachowania jest fakt, e niewirtualne funkcje  


i   s wizane statycznie (patrz sposb 38.). Oznacza to, e poniewa zmienna 
zostaa zadeklarowana jako wskanik do , niewirtualne funkcje wywoywane za porednictwem tej zmiennej zawsze bd tymi zdefiniowanymi dla klasy , nawet jeli
 wskazuje na obiekt klasy pochodnej wzgldem  (jak w powyszym przykadzie).
Z drugiej strony, funkcje wirtualne s wizane dynamicznie (ponownie patrz sposb 38.),
co oznacza, e opisywany problem ich nie dotyczy. Gdyby  bya funkcj wirtualn,
jej wywoanie (niezalenie od tego, czy z wykorzystaniem wskanika do  czy do )
spowodowaoby wywoanie wersji  , poniewa  i  w rzeczywistoci wskazuj
na obiekt klasy .
Naley pamita, e jeli tworzymy klas  i ponownie definiujemy dziedziczon po
klasie  niewirtualn funkcj , obiekty klasy  bd si prawdopodobnie okazyway
zachowania godne schizofrenika. W szczeglnoci dowolny obiekt klasy  moe
w odpowiedzi na wywoanie funkcji  zachowywa si albo jak obiekt klasy ,
albo jak obiekt klasy ; czynnikiem rozstrzygajcym nie bdzie tutaj sam obiekt, ale
zadeklarowany typ wskazujcego na ten obiekt wskanika. Rwnie zdumiewajce zachowanie zaprezentowayby w takim przypadku referencje do obiektw.
To ju wszystkie argumenty wysuwane przez praktykw. Chcesz pewnie teraz pozna
jakie teoretyczne uzasadnienie, dlaczego nie naley ponownie definiowa dziedziczonych funkcji niewirtualnych. Wyjani to z przyjemnoci.
W sposobie 35. pokazaem, e publiczne dziedziczenie oznacza w rzeczywistoci
relacj jest; w sposobie 36. opisaem, dlaczego deklarowanie niewirtualnych funkcji w klasie powoduje niezaleno od ewentualnych specjalizacji tej klasy. Jeli waciwie wykorzystasz wnioski wyniesione z tych sposobw podczas projektowania
klas  i  oraz podczas tworzenia niewirtualnej funkcji skadowej  , wwczas:
 Wszystkie funkcje, ktre mona stosowa dla obiektw klasy , mona
stosowa take dla obiektw klasy , poniewa kady obiekt klasy  jest
obiektem klasy .
 Podklasy klasy  musz dziedziczy zarwno interfejs, jak i implementacj
funkcji , poniewa funkcja ta zostaa zadeklarowana w klasie  jako niewirtualna.

Jeli w klasie  ponownie zdefiniujemy teraz funkcj , w naszym projekcie powstanie sprzeczno. Jeli klasa  faktycznie potrzebuje wasnej implementacji funkcji ,
ktra bdzie si rnia od implementacji dziedziczonej po klasie , oraz jeli kady
obiekt klasy  (niezalenie od poziomu specjalizacji) rzeczywicie musi wykorzystywa implementacji tej funkcji z klasy , wwczas stwierdzenie, e  jest  jest zwyczajnie nieprawdziwe. Klasa  nie powinna w takim przypadku publicznie dziedziczy po klasie . Z drugiej strony, jeli  naprawd musi publicznie dziedziczy po 
oraz jeli  naprawd musi implementowa funkcj  inaczej, ni implementuje j
klasa , wwczas nieprawd jest, e  odzwierciedla niezaleno od specjalizacji
klasy . W takim przypadku funkcja  powinna zosta zadeklarowana jako wirtualna.
Wreszcie, jeli kady obiekt klasy  naprawd musi by w relacji jest z obiektem
klasy  oraz jeli funkcja  rzeczywicie reprezentuje niezaleno od specjalizacji
klasy , wwczas klasa  nie powinna potrzebowa wasnej implementacji funkcji 
i jej projektant nie powinien wic podejmowa podobnych prb.

176

Dziedziczenie i projektowanie zorientowane obiektowo

Niezalenie od tego, ktry argument najbardziej pasuje do naszej sytuacji, oczywiste


jest, e ponowne definiowanie dziedziczonych funkcji niewirtualnych jest cakowicie
pozbawione sensu.

Sposb 38.
Nigdy nie definiuj ponownie
dziedziczonej domylnej wartoci parametru
Sposb 38. Nigdy nie definiuj ponownie dziedziczonej domylnej wartoci parametru

Sprbujmy uproci nasze rozwaania od samego pocztku. Domylny parametr moe


istnie wycznie jako cz funkcji, a nasze klasy mog dziedziczy tylko dwa rodzaje funkcji wirtualne i niewirtualne. Jedynym sposobem ponownego zdefiniowania wartoci domylnej parametru jest wic ponowne zdefiniowanie caej dziedziczonej funkcji. Ponowne definiowanie dziedziczonej niewirtualnej funkcji jest jednak
zawsze bdne (patrz sposb 37.), moemy wic od razu ograniczy nasz analiz do
sytuacji, w ktrej dziedziczymy funkcj wirtualn z domyln wartoci parametru.
W takim przypadku wyjanienie sensu umieszczania tego sposobu w ksice jest bardzo proste funkcje wirtualne s wizane dynamicznie, ale domylne wartoci ich
parametrw s wizane statycznie.
Co to oznacza? By moe nie posugujesz si biegle najnowszym argonem zwizanym z programowaniem obiektowym lub zwyczajnie zapomniae, jakie s rnice
pomidzy wizaniem statycznym a wizaniem dynamicznym. Przypomnijmy wic
sobie, o co tak naprawd chodzi.
Typem statycznym obiektu jest ten typ, ktry wykorzystujemy w deklaracji obiektu
w kodzie programu. Przeanalizujmy ponisz hierarchi klas:
2D   0=<(I0==.(+JK= 
!
&$,## !

2

%!,#!$ '
"&$&,
&
 %2D    50=<
58


0
#2

!%:"%#'


 4
$% 4"?F*
 %2D    5I0==.



D2

 %2D    




Sposb 38. Nigdy nie definiuj ponownie dziedziczonej domylnej wartoci parametru

177

Powysz hierarchi mona przedstawi graficznie:

Rozwamy teraz ponisze wskaniki:


2>!
52>
2>5
%D!
52>
2>5
%0
#!
52>

W powyszym przykadzie  ,  i  s zadeklarowane jako zmienne typu wskanikowego do obiektw klasy , zatem wszystkie nale do typu statycznego.
Zauwa, e nie ma w tym przypadku znaczenia, na co wymienione zmienne faktycznie wskazuj ich statycznym typem jest .
Typ dynamiczny obiektu zaley od typu obiektu aktualnie przez niego wskazywanego.
Oznacza to, e od dynamicznego typu zaley zachowanie obiektu. W powyszym
przykadzie typem dynamicznym zmiennej  jest #, za typem dynamicznym
zmiennej  jest  
. Inaczej jest w przypadku zmiennej  , ktra nie ma
dynamicznego typu, poniewa w rzeczywistoci nie wskazuje na aden obiekt.
Typy dynamiczne (jak sama nazwa wskazuje) mog si zmienia w czasie wykonywania programu, tego rodzaju zmiany odbywaj si zazwyczaj na skutek wykonania
operacji przypisania:
5
!
%F

&!D>
5
!
%F

&!0
#>

Wirtualne funkcje s wizane dynamicznie, co oznacza, e konkretna wywoywana


funkcja zaley od dynamicznego typu obiektu wykorzystywanego do jej wywoania:
?@%0=<%% )&,
&'D%0=<
?@%0=<%% )&,
&'0
#%0=<

Uwaasz pewnie, e nie ma powodw wraca do tego tematu z pewnoci rozumiesz ju znaczenie funkcji wirtualnych. Problemy ujawniaj si dopiero w momencie,
gdy analizujemy funkcje wirtualne z domylnymi wartociami parametrw, poniewa
jak ju wspomniaem funkcje wirtualne s wizane dynamicznie, a domylne
parametry C++ wie statycznie. Oznacza to, e moesz wywoa wirtualn funkcj
zdefiniowan w klasie potomnej, ale z domyln wartoci parametru z klasy bazowej:
?@%%% )&0
#%0=<

178

Dziedziczenie i projektowanie zorientowane obiektowo

W tym przypadku typem dynamicznym zmiennej wskanikowej  jest  


,
zatem zostanie wywoana (zgodnie z naszymi oczekiwaniami) funkcja wirtualna 
zdefiniowana w klasie  
. Domyln wartoci parametru funkcji  
&
  jest '((). Poniewa jednak typem statycznym zmiennej  jest ,
domylna warto parametru dla tego wywoania funkcji bdzie pochodzia z definicji
klasy , a nie  
! Otrzymujemy w efekcie wywoanie skadajce si z nieoczekiwanej kombinacji dwch deklaracji funkcji  z klas  i  
.
Moesz mi wierzy, tworzenie oprogramowania zachowujcego si w taki sposb jest
ostatni rzecz, ktr chciaby robi; jeli to Ci nie przekonuje, zaufaj mi na pewno
z takiego zachowania Twojego oprogramowania nie bd zadowoleni Twoi klienci.
Nie musz chyba dodawa, e nie ma w tym przypadku adnego znaczenia fakt, e
 ,  i  s wskanikami. Gdyby byy referencjami, problem nadal by istnia. Jedynym istotnym rdem naszego problemu jest to, e  jest funkcj wirtualn i jedna

z jej domylnych wartoci parametrw zostaa ponownie zdefiniowana w podklasie.


Dlaczego C++ umoliwia tworzenie oprogramowania zachowujcego si w tak nienaturalny sposb? Odpowiedzi jest efektywno wykonywania programw. Gdyby domylne
wartoci parametrw byy wizane dynamicznie, kompilatory musiayby stosowa
dodatkowe mechanizmy okrelania waciwych domylnych wartoci parametrw
funkcji wirtualnych podczas wykonywania programw, co prowadzioby do spowolnienia
i komplikacji stosowanego obecnie mechanizmu ich wyznaczania w czasie kompilacji.
Decyzj podjto wic wycznie z myl o szybkoci i prostocie implementacji. Efektem
jest wydajny mechanizm wykonywania programw, ale take jeli nie bdziesz
stosowa zalece zawartych w tym sposobie potencjalne nieporozumienia.

Sposb 39.
Unikaj rzutowania
w d hierarchii dziedziczenia
Sposb 39. Unikaj rzutowania w d hierarchii dziedziczenia

W dzisiejszych niespokojnych czasach warto mie na oku poczynania instytucji


finansowych, rozwamy wic klas protokou (patrz sposb 34.) dla kont bankowych:

   
+
B 


+
B 


>E%
(


>& 
E%

L+
B 

 <   
58
 32%  
58
 

58



Sposb 39. Unikaj rzutowania w d hierarchii dziedziczenia

179

Wiele bankw przedstawia dzisiaj swoim klientom niezwykle szerok ofert typw
kont bankowych, zamy jednak (dla uproszczenia), e istnieje tylko jeden typ konta
bankowego, zwyke konto oszczdnociowe:

#B 
+
B 



#B 


>E%
(


>& 
E%

L
#B 

 ;
 &  




Nie jest to moe zbyt zaawansowane konto oszczdnociowe, jest jednak w zupenoci wystarczajce dla naszych rozwaa.
Bank prawdopodobnie przechowuje list wszystkich swoich kont, ktra moe by zaimplementowana za pomoc szablonu klasy  ze standardowej biblioteki C++ (patrz
sposb 49.). Przypumy, e w naszym banku taka lista nosi nazw !
:
M+
B 
>@B 
%!
 )#%

!!



Jak wszystkie standardowe pojemniki, listy przechowuj jedynie kopie umieszczanych


w nich obiektw, zatem, aby unikn przechowywania wielu kopii poszczeglnych
obiektw klasy 
!
, programici zdecydowali, e lista !
powinna
skada si jedynie ze wskanikw do tych obiektw, a nie samych obiektw reprezentujcych konta.
Przypumy, e naszym zadaniem jest kolejne przejcie przez wszystkie konta i doliczenie
do nich nalenych odsetek. Moemy zrealizowa t usug w nastpujcy sposb:
'
! 
  %
&4
#%!4
&
)4
 !

! % !&$ (!
&
, M+
B 
>@ 5B 
 #

*5B 
 

66
>?@;
)$*

Nasze kompilatory szybko zasygnalizuj, e lista !


zawiera wskaniki do
obiektw klasy 
!
, a nie obiektw klasy 
 !
, zatem w kadej
kolejnej iteracji zmienna  bdzie wskazywaa na obiekt klasy 
!
. Oznacza
to, e wywoanie funkcji  
 jest nieprawidowe, poniewa zostaa ona
zadeklarowana wycznie dla obiektw klasy 
 !
, a nie 
!
.
Jeli wiersz  *
!
+   ,,-,!
.
/0 jest dla Ciebie
niezrozumiay i nie przypomina Ci kodu C++, z ktrym miae do czynienia do tej pory,
oznacza to, e prawdopodobnie nie miae wczeniej przyjemnoci korzysta z szablonw klas pojemnikowych ze standardowej biblioteki C++. T cz biblioteki nazywa
si czsto Standardow Bibliotek Szablonw (ang. Standard Template Library STL),
wicej informacji na jej temat znajdziesz w sposobie 49. Na tym etapie wystarczy Ci

180

Dziedziczenie i projektowanie zorientowane obiektowo

wiedza, e zmienna  zachowuje si jak wskanik wskazujcy na przegldane w ptli


kolejne elementy listy !
(od jej pocztku do koca). Oznacza to, e zmienna  jest traktowana tak, jakby jej typem by 
!
, a elementy przegldanej
listy byy przechowywane w tablicy.
Powyszy kod nie zostanie niestety skompilowany. Wiemy oczywicie, e lista
!
zostaa zdefiniowana jako pojemnik na wskaniki typu 
!
,
ale mamy take wiadomo, e w powyszej ptli faktycznie przechowuje wskaniki
typu 
 !
, poniewa 
 !
jest jedyn klas, dla ktrej moemy
tworzy obiekty. Gupie kompilatory! Zdecydowalimy si przekaza im wiedz, ktra jest dla nas zupenie oczywista i okazao si, e s zbyt tpe, by zauway, e lista
!
przechowuje w rzeczywistoci wskaniki typu 
 !
:
'! 
  %
(2 
&   !%$!

, M+
B 
>@ 5B 
 #

*5B 
 

66
NM
#B 
>@>?@;


Rozwizalimy wszystkie nasze problemy! Zrobilimy to przejrzycie, elegancko i zwile


wystarczyo jedno proste rzutowanie. Wiemy, jakiego typu wskaniki faktycznie
znajduj si na licie !
, nasze ogupiae kompilatory tego nie wiedz, wic
zastosowalimy rzutowanie, by przekaza im nasz wiedz. Czy mona znale bardziej logiczne rozwizanie?
Chciabym w tym momencie przedstawi pewn biblijn analogi. Operacje rzutowania s dla programistw C++ jak jabko dla biblijnej Ewy.
Zaprezentowany w powyszym kodzie rodzaj rzutowania (ze wskanika do klasy bazowej
do wskanika do klasy potomnej) nosi nazw rzutowania w d, poniewa rzutujemy
w d hierarchii dziedziczenia. W powyszym przykadzie operacja rzutowania w d
zadziaaa, jednak jak si za chwil przekonasz takie rozwizanie prowadzi do
ogromnych problemw podczas konserwacji oprogramowania.
Wrmy jednak do naszego banku. Zachcony sukcesem, jakim byo rozwizanie problemu kont oszczdnociowych, bank postanawia zaoferowa swoim klientom take
konta czekowe. Co wicej, zamy, e do tego typu kont take dolicza si nalene odsetki (podobnie, jak w przypadku kont oszczdnociowych):
D2
#B 
+
B 


 ;
 &  




Nie musz chyba dodawa, e lista !


bdzie teraz zawieraa wskaniki
zarwno do obiektw reprezentujcych konta oszczdnociowe, jak i do tych reprezentujcych konta czekowe. Okazuje si, e stworzona przed chwil ptla naliczajca
odsetki przestaje dziaa.

Sposb 39. Unikaj rzutowania w d hierarchii dziedziczenia

181

Pierwszy problem polega na tym, e ptla nadal bdzie poprawnie kompilowana, mimo
e nie wprowadzilimy jeszcze zmian uwzgldniajcych istnienie obiektw nowej
klasy #
!
. Wynika to z faktu, e nasze kompilatory nadal bd pochopnie
zakaday, e kiedy sygnalizujemy (za pomoc  1 ), e  w rzeczywistoci
wskazuje na 
 !
, tak jest w istocie. W kocu to do nas naley przewidywanie skutkw wykonywania poszczeglnych instrukcji. Drugi problem wynika
z typowego sposobu radzenia sobie z pierwszym problemem, co zwykle skutkuje
tworzeniem podobnego kodu:
, M+
B 
>@ 5B 
 #

*5B 
 

66
,> 

 
#B 

NM
#B 
>@>?@;


NMD2
#B 
>@>?@;


Jeli kiedykolwiek stwierdzisz, e Twj kod zawiera instrukcj warunkow w postaci:


jeli obiekt jest typu T1, zrb co, jeli jednak jest typu T2, zrb co innego, natychmiast uderz si w pier. Tego rodzaju instrukcje s nienaturalne dla jzyka C++;
podobn strategi mona stosowa w C lub Pascalu, ale nigdy w C++, w ktrym moemy przecie uy funkcji wirtualnych.
Pamitaj, e zastosowanie funkcji wirtualnych sprawia, e za zapewnianie prawidowych wywoa funkcji w zalenoci od typu wykorzystywanego obiektu
odpowiadaj kompilatory. Nie zamiecaj wic swojego kodu instrukcjami warunkowymi ani przecznikami; zamiast tego wykorzystuj moliwoci swoich kompilatorw. Oto przykad:
+
B 
   &%&

%!
&$
!
!$
  
;
+
#B 
+
B 


 ;
58



#B 
;
+
#B 

 &%&

D2
#B 
;
+
#B 

 &%&


Powysz hierarchi mona przedstawi graficznie:

182

Dziedziczenie i projektowanie zorientowane obiektowo

Poniewa zarwno konta oszczdnociowe, jak i konta czekowe wymagaj naliczania


odsetek, naturalnym rozwizaniem jest przeniesienie wsplnej operacji na wyszy
poziom hierarchii klas do wsplnej klasy bazowej. Jednak przy zaoeniu, e nie
wszystkie konta w danym banku musz mie naliczane odsetki (jak wynika z moich
dowiadcze, takie zaoenie jest sensowne), nie moemy przenie wspomnianej
operacji do najwyszej (w hierarchii) klasy 
!
. Wprowadzilimy wic now
podklas klasy 
!
, ktr nazwalimy 
 
!
, i ktra jest
klas bazow dla klas 
 !
i #
!
.
Wymaganie, by zarwno dla konta oszczdnociowego, jak i dla konta czekowego
nalicza odsetki, uwzgldnilimy, deklarujc w klasie 
 
!
czyst
funkcj wirtualn  
 , ktra zostanie przypuszczalnie zdefiniowana w podklasach 
 !
i #
!
.
Nowa hierarchia klas umoliwia nam napisanie omawianej wczeniej ptli od pocztku:
 !%$!
!(

 
)
, M+
B 
>@ 5B 
 #

*5B 
 

66
NM;
+
#B 
>@>?@;


Mimo e powysza ptla nadal zawiera skrytykowane wczeniej rzutowanie, zaproponowane rozwizanie jest znacznie lepsze od omawianych do tej pory, poniewa bdzie
poprawnie funkcjonowao nawet po dodaniu do naszej aplikacji nowych podklas do
klasy 
 
!
.
Aby cakowicie pozby si rzutowania, musimy wprowadzi do naszego projektu kilka
dodatkowych modyfikacji. Jedn z nich jest doprecyzowanie specyfikacji wykorzystywanej listy kont. Gdybymy mogli wykorzysta list obiektw klasy 
 &

!
, zamiast obiektw klasy 
!
, rozwizanie byoby trywialne:
%! )#%
!!

! 
M;
+
#B 
>@;+B 

'! 
  %
'!!))%) %
, M;
+
#B 
>@ 5;+B 
 #

*5;+B 
 

66
>?@;


Sposb 39. Unikaj rzutowania w d hierarchii dziedziczenia

183

Jeli tworzenie bardziej specjalizowanej listy nie jest brane pod uwag, mona stwierdzi, e operacj  
 mona stosowa dla wszystkich kont bankowych;
jednak w przypadku kont, dla ktrych nie nalicza si odsetek, wspomniana operacja
jest pusta. To samo moemy wyrazi za pomoc poniszego fragmentu kodu:
+
B 


 ;




#B 
+
B 
   
D2
#B 
+
B 
   
M+
B 
>@B 

!(
! %
*
, M+
B 
>@ 5B 
 #

*5B 
 

66
>?@;


Zauwa, e wirtualna funkcja 


!
 
 udostpnia pust domyln implementacj. Jest to wygodny sposb definiowania funkcji, ktra domylnie
jest operacj pust, moe jednak w przyszoci doprowadzi do nieprzewidywalnych
trudnoci. Omwienie przyczyn takiego niebezpieczestwa i sposobw jego eliminowania znajdziesz w sposobie 36. Zauwa take, e funkcja  
 jest (niejawnie) wbudowana. Oczywicie nie ma w tym nic zego, jednak z uwagi na fakt, e
omawiana funkcja jest take wirtualna, dyrektywa 

 bdzie prawdopodobnie
zignorowana przez kompilator (patrz sposb 33.).
Jak si przekonae, rzutowanie w d hierarchii klas moe by eliminowane na wiele
sposobw. Najlepszym z nich jest zastpienie tego typu operacji wywoaniami funkcji
wirtualnych mona wwczas zdefiniowa domylne implementacje tych funkcji
jako operacje puste, co pozwoli na ich prawidow obsug przez obiekty klasy, dla
ktrych dane dziaania nie maj sensu. Drug metod jest ucilenie wykorzystywanych typw, co pozwala wyeliminowa nieporozumienia wynikajce z rozbienoci
pomidzy typami deklarowanymi a typami reprezentowanymi przez uywane obiekty
w rzeczywistoci. Wysiek zwizany z eliminowaniem rzutowania w d nie idzie na
marne, poniewa kod zawierajcy operacje tego typu jest brzydki i moe by rdem
bdw, jest take trudny do zrozumienia, rozszerzania i konserwacji.
To, co napisaem do tej pory, jest prawd i tylko prawd. Nie jest jednak ca prawd. Istniej bowiem sytuacje, w ktrych naprawd musisz wykona operacj rzutowania w d.
Przykadowo, przypumy, e mamy do czynienia z rozwaan na pocztku tego sposobu sytuacj, w ktrej lista !
przechowuje wskaniki do obiektw klasy

!
, funkcja  
 jest zdefiniowana wycznie dla obiektw klasy

 !
i musimy napisa ptl naliczajc odsetki dla wszystkich kont.
Przypumy take, e wszystkie wspomniane elementy s poza nasz kontrol, co

184

Dziedziczenie i projektowanie zorientowane obiektowo

oznacza, e nie moemy modyfikowa definicji 


!
, 
 !
ani
!
(jest to sytuacja charakterystyczna, kiedy korzystamy z elementw zdefiniowanych w bibliotece, do ktrej mamy dostp tylko do odczytu). W takim przypadku musielibymy zastosowa rzutowanie w d, niezalenie od katastrofalnych
skutkw takiego posunicia.
Niezalenie od tego istnieje lepszy sposb ni stosowane wczeniej surowe rzutowanie.
Tym sposobem jest co, co nazywamy bezpiecznym rzutowaniem w d wymaga
zastosowania zaimplementowanego w C++ operatora 
1 . Kiedy stosujemy
ten operator dla wskanika, wykonywane jest rzutowanie, ktre w przypadku
powodzenia (tzn. jeli dynamiczny typ wskanika, patrz sposb 38., jest zgodny z typem,
do ktrego jest rzutowany) powoduje zwrcenie poprawnego wskanika nowego
typu; w przypadku niepowodzenia operacji, zwracany jest wskanik pusty.
Oto przykad z kontami bankowymi po dodaniu mechanizmu bezpiecznego rzutowania w d:
+
B 
   &
 !$#   

#B 

+
B 
   &%&
D2
#B 
&%&
+
B 
   
M+
B 
>@B 
%#$!
& 
  

##,
& )#&$)'!
&
:(!
&
&! %
&!!!

, M+
B 
>@ 5B 
 #

*5B 
 

66
:!!
# ! %
%:)%F
>

#B 

, &
,
&
!
&!!
&
,
#B 
>5

NM
#B 
>@>
?@;


:!!
# ! %
%:) D2
#B 

,D2
#B 
>5

NMD2
#B 
>@>
?@;



(
!




 /.!


*/


Powyszy schemat daleki jest od ideau, ale przynajmniej umoliwia wykrywanie


operacji rzutowania w d zakoczonych niepowodzeniem, co byo niemoliwe, kiedy
wykorzystywalimy operator  1 zamiast operatora 
1 . Zauwa

Sposb 39. Unikaj rzutowania w d hierarchii dziedziczenia

185

jednak, e rozsdek nakazuje nam take sprawdzenie przypadku, w ktrym wszystkie


operacje rzutowania zakoczyy si niepowodzeniem. Realizujemy to zadanie w powyszym kodzie za pomoc ostatniej klauzuli  . Taka weryfikacja bya zbdna
w przypadku wirtualnych funkcji, poniewa kade wywoanie takiej funkcji musiao
dotyczy jakiej jej wersji. Kiedy jednak decydujemy si na rzutowanie w d, nasza
sytuacja zmienia si diametralnie kiedy kto doda do hierarchii np. nowy typ konta,
ale zapomni o aktualizacji powyszego kodu, wszystkie rzutowania w d zakocz
si niepowodzeniem. Dlatego wanie tak wana jest obsuga opisywanej sytuacji.
Jest bardzo mao prawdopodobne, by wszystkie operacje rzutowania zakoczyy si
niepowodzeniem, jeli jednak decydujemy si na rzutowanie w d, nawet najlepszym
programistom moe si przytrafi co niedobrego.
Czy przecierasz z niedowierzania oczy, widzc, jak wygldaj definicje zmiennych
w warunkach powyszych instrukcji ? Jeli tak, nie martw si, dobrze widzisz.
Moliwo definiowania takich zmiennych zostaa dodana do jzyka C++ w tym samym momencie, co operator 
1 . Moemy dziki temu pisa elegancki kod,
poniewa wskaniki   i  w rzeczywistoci nie s nam potrzebne a do momentu
wywoania pomylnie inicjalizujcego je operatora 
1 . Nowa skadnia
sprawia, e nie musimy definiowa tych zmiennych poza instrukcjami warunkowymi
zawierajcymi operacje rzutowania (w sposobie 32. wyjaniem, dlaczego powinnimy unika niepotrzebnych definicji zmiennych). Jeli Twoje kompilatory jeszcze nie
obsuguj takiego sposobu definiowania zmiennych, moesz wykorzysta star metod:
, M+
B 
>@ 5B 
 #

*5B 
 

66

#B 
>,
&&

D2
#B 
>,
&&

,5
NM
#B 
>@>
?@;


,5
NMD2
#B 
>@>
?@;



 /.!


*/


W tym przypadku miejsce definiowania zmiennych podobnych do   i  nie jest


oczywicie najwaniejsze. Istotne jest co zupenie innego rzutowanie w d prowadzi do stylu programowania opartego na klauzulach & 
& , co jest najgorszym moliwym rozwizaniem w ciaach funkcji wirtualnej i powinno by zarezerwowane dla sytuacji, w ktrych naprawd nie istnieje rozwizanie alternatywne. Na
szczcie, przy odrobinie szczcia nigdy nie bdziesz musia zmaga si z tak ponurym obliczem programowania.

186

Dziedziczenie i projektowanie zorientowane obiektowo

Sposb 40.
Modelujc relacje posiadania (ma)
i implementacji z wykorzystaniem,
stosuj podzia na warstwy
Sposb 40. Modelujc relacje posiadania (ma) i implementacji...

Dzielenie na warstwy jest procesem budowania pewnych klas ponad innymi klasami
w taki sposb, e cz klas zawiera w postaci skadowych reprezentujcych dane
obiekty klas znajdujcych si na innych warstwach. Przykadowo:
B   !
&!!

2
.   






#
 !

&%%
B& %
2
. .& %
2
.,G.& %


W powyszym przykadzie klasa  


znajduje si w warstwie ponad klasami

, ! i 
) , poniewa zawiera skadowe reprezentujce dane
trzech wymienionych typw. Pojcia dzielenia na warstwy ma wiele synonimw,
uywa si take okrele skadania, zawierania, agregacji i osadzania klas.
W sposobie 35. wyjaniem, e publiczne dziedziczenie faktycznie oznacza relacj jest.
Inaczej jest w przypadku podziau na warstwy ten sposb wizania klas oznacza
w rzeczywistoci albo relacj ma, albo relacj implementacji z wykorzystaniem.
Zaprezentowana powyej klasa  
jest przykadem relacji ma. Obiekty tej klasy
zawieraj dane o nazwisku, adresie oraz numerze zwykego telefonu i faksu. Nie moemy powiedzie, e dana osoba jest nazwiskiem lub jest adresem. Powiedzielibymy
raczej, e osoba ta ma nazwisko lub ma adres etc. Dla wikszoci programistw takie
rozrnienie nie stanowi wikszego problemu, zatem nieporozumienia odnonie znacze relacji jest i ma s stosunkowo rzadkie.
Znacznie trudniejsze jest jasne okrelenie rnicy pomidzy relacj jest a relacj
implementacji z wykorzystaniem. Przykadowo przypumy, e potrzebujemy szablonu dla klas reprezentujcych zbiory dowolnych obiektw, czyli kolekcje bez powtrze. Poniewa zdolno ponownego wykorzystywania gotowych rozwiza jest
jedn z najbardziej podanych cech kadego programisty i poniewa pewnie zapoznae si ju z treci sposobu 49. powiconego standardowej bibliotece C++,
Twoim pierwszym pomysem bdzie wykorzystanie dostpnego w tej bibliotece szablonu  . Po co miaby pisa nowy szablon, jeli moesz wykorzysta dobrej jakoci
szablon napisany przez kogo innego?

Sposb 40. Modelujc relacje posiadania (ma) i implementacji...

187

Kiedy zagbisz si w dokumentacji szablonu  , odkryjesz jednak pewne ograniczenia tej struktury, ktre s nie do przyjcia w Twojej aplikacji struktura 
wymaga, by przechowywane w niej elementy byy cakowicie uporzdkowane, co
oznacza, e dla kadej pary obiektw  i  nalecych do struktury  musi istnie
moliwo okrelenia, czy ,*, i czy ,*,. W przypadku wielu typw spenienie takiego wymagania jest bardzo atwe, a posiadanie cakowicie uporzdkowanych obiektw pozwala strukturze  na zapewnianie bardzo korzystnych warunkw w zakresie
efektywnoci dziaania (wicej szczegw na temat wydajnoci struktur udostpnianych przez standardow bibliotek C++ znajdziesz w sposobie 49.). Potrzebujesz
jednak struktury bardziej oglnej, klasy podobnej do  , w ktrej przechowywane
obiekty nie musz spenia relacji cakowitego porzdku, a jedynie takie, dla ktrych
mona wyznaczy relacj rwnoci (dla ktrych istnieje moliwo okrelenia, czy
-- dla obiektw  i  tego samego typu). To skromniejsze wymaganie znacznie lepiej pasuje do typw reprezentujcych np. kolory. Czy czerwony jest mniejszy od
zielonego, czy te zielony jest mniejszy od czerwonego? Wyglda na to, e w takim
przypadku bdziemy musieli ostatecznie opracowa wasny szablon.
Ponowne wykorzystywanie gotowych rozwiza jest nadal wietnym rozwizaniem.
Jeli jeste ekspertem w dziedzinie struktur danych, z pewnoci wiesz, e niemal
nieograniczone moliwoci implementowania zbiorw daje (stosunkowo prosta) struktura listy jednokierunkowej. Co wicej, szablon  (generujcy klasy list jednokierunkowych) jest dostpny w standardowej bibliotece C++! Decydujesz si wic na jego
(ponowne) wykorzystanie.
W szczeglnoci postanawiasz, e Twj nowy szablon  bdzie dziedziczy po szablonie  . Oznacza to, e struktura  *2+ bdzie dziedziczya po  *2+. Twoja
implementacja zakada w kocu, e obiekt  w rzeczywistoci bdzie obiektem
 . Deklarujesz wic swj szablon  w nastpujcy sposb:

%)4% :% !
!

MO@
MO@   

By moe wszystko na tym etapie wyglda prawidowo, jednak w rzeczywistoci zaprezentowane rozwizanie zawiera zasadnicze usterki. W sposobie 35. wyjaniem, e
jeli D jest B, wszystkie poprawne wasnoci klasy B s take poprawne dla klasy D.
Obiekt klasy  moe jednak zawiera duplikaty, zatem jeli warto 3051 zostanie
wstawiona do struktury  *
+ dwukrotnie, reprezentowana lista bdzie zawieraa
dwie kopie tej liczby. Inaczej jest w przypadku struktury  , ktra nie moe zawiera
duplikatw jeli wic warto 3051 zostanie wstawiona do  *
+ dwukrotnie,
reprezentowany zbir i tak bdzie zawiera tylko jedn jej kopi. Stwierdzenie, e 
jest  jest wic faszywe, poniewa istniej wasnoci poprawne dla obiektw
klasy  , ktre nie s poprawne dla obiektw klasy  .
Poniewa omawiane dwie klasy s zwizane relacj inn ni jest, modelowanie rzeczywistej relacji nie powinno si opiera na mechanizmie publicznego dziedziczenia.
Waciwym rozwizaniem jest stwierdzenie, e obiekt klasy  moe by implementowany z wykorzystaniem obiektu klasy  :

188

Dziedziczenie i projektowanie zorientowane obiektowo


%)4% :% !

MO@


 
O

 

O
  
O





MO@!
&! 


Funkcje skadowe klasy  mog w duej mierze opiera si na funkcjonalnoci udostpnianej zarwno przez szablon  , jak i innych czci standardowej biblioteki
C++, zatem implementacja powyszej klasy nie jest trudna do napisania ani do pniejszego zrozumienia:
MO@
 MO@
O


,
 #
( 
(*5 

MO@
 MO@

O
,* 2N
MO@
 MO@ 
O

MO@ 5,
 #
( 
(
,*5 
 

MO@
 MO@



 !

Powysze funkcje s na tyle proste, e warto rozway ich wbudowanie, chocia


zdaj sobie spraw, e przed podjciem takiej decyzji powiniene przypomnie sobie
nasze rozwaania ze sposobu 33. (wykorzystane w powyszym kodzie funkcje 
,

, 
,  1 etc. s czci udostpnianego przez standardow bibliotek
C++ modelu dla szablonw generujcych pojemniki podobne do  oglny opis
tego modelu znajdziesz w sposobie 49.).
Musimy take pamita, e interfejs klasy  nie spenia wymaga stawianych przed
interfejsem kompletnym i minimalnym (patrz sposb 18.). Gwnym zaniedbaniem
w zakresie kompletnoci jest brak funkcjonalnoci obsugujcej przechodzenie przez
kolejne obiekty przechowywane w zbiorze, co w wielu programach moe by niezbdne (i jest oferowane przez wszystkie podobne struktury standardowej biblioteki
C++, wcznie z szablonem  ). Kolejnym niedocigniciem jest niezgodno klasy
 z przyjtymi w standardowej bibliotece konwencjami dotyczcymi klas pojemnikowych (patrz sposb 49.), co utrudnia uytkownikom struktury  korzystanie z innych czci tej biblioteki.

Sposb 41. Rozrniaj dziedziczenie od stosowania szablonw

189

Wady interfejsu klasy  nie powinny jednak przesoni niekwestionowanej zalety


tej struktury relacji pomidzy klasami  i  . Nie jest to relacja jest (cho
pocztkowo moga na tak wyglda), mamy w tym przypadku do czynienia z relacj
implementacji z wykorzystaniem, za zastosowanie do jej implementacji mechanizmu
podziau na warstwy moe by rdem uzasadnionej dumy u kadego projektanta klas.
Nawiasem mwic, w sytuacjach, kiedy wykorzystujesz podzia na warstwy do modelowania relacji pomidzy klasami, tworzysz czc je zaleno czasu kompilacji.
Informacje na temat niepodanych skutkw takiego dziaania oraz moliwoci ich
unikania znajdziesz w sposobie 34.

Sposb 41.
Rozrniaj dziedziczenie
od stosowania szablonw
Sposb 41. Rozrniaj dziedziczenie od stosowania szablonw

Przeanalizuj dwa ponisze problemy zwizane z projektowaniem.


 Jeste sumiennym studentem informatyki i chcesz stworzy klasy reprezentujce

stosy obiektw. Bdziesz potrzebowa wielu rnych klas, poniewa kady


stos musi by homogeniczny, co oznacza, e moe zawiera obiekty tylko
jednego typu. Przykadowo, moesz opracowa klas dla stosw liczb
cakowitych typu 
, klas dla stosw acuchw znakowych typu 

oraz klas reprezentujc stosy stosw acuchw typu 
. Planujesz
jedynie opracowanie minimalnego interfejsu tej klasy (patrz sposb 18.),
ograniczasz wic dostpne operacje do tworzenia stosu, niszczenia stosu,
pooenia obiektu na stosie, zdjcia obiektu ze stosu oraz okrelenia, czy
stos jest pusty. Rezygnujesz ze stosowania klas dostpnych w standardowej
bibliotece C++ (wcznie z klas  patrz sposb 49.), poniewa
pragniesz zdoby dowiadczenie w samodzielnym tworzeniu zaawansowanych
struktur danych. Ponowne wykorzystywanie gotowych rozwiza jest
oczywicie doskonaym sposobem tworzenia oprogramowania, jeli jednak
Twoim celem jest dogbna analiza pewnych zachowa, nie ma nic lepszego
ni samodzielne ich kodowanie.
 Jeste mionikiem kotw i chcesz opracowa klasy reprezentujce koty.

Bdziesz potrzebowa wielu rnych klas, poniewa kada rasa kotw


jest nieco inna. Jak wszystkie obiekty, koty mog by tworzone i niszczone
oraz co jest oczywiste dla kadego mionika kotw mog dodatkowo
wycznie je i spa. Koty kadej rasy jedz i pi w charakterystyczny
dla siebie, ujmujcy sposb.
Opisane specyfikacje problemw brzmi podobnie, jednak na ich podstawie naley
opracowa cakowicie odmienne projekty oprogramowania. Dlaczego?
Odpowied wynika z relacji pomidzy zachowaniami poszczeglnych klas a typami
przetwarzanych obiektw. Zarwno w przypadku stosw, jak i w przypadku kotw operujemy na zupenie innych typach (stosy zawieraj obiekty typu 2, koty reprezentuj

190

Dziedziczenie i projektowanie zorientowane obiektowo

obiekty rasy 2), jednak pytanie, ktre musimy sobie postawi, brzmi nastpujco:
Czy typ 2 wpywa na zachowanie tworzonej klasy?. Jeli 2 nie wpywa na zachowanie klasy, moemy zastosowa szablon. Jeli 2 ma wpyw na zachowanie projektowanej klasy, bdziemy potrzebowali wirtualnych funkcji, co oznacza, e konieczne
bdzie wykorzystanie mechanizmu dziedziczenia.
Oto jak moemy zdefiniowa opart na licie jednokierunkowej implementacj klasy
, zakadajc, e przechowywane na stosie obiekty s typu 2:



L
 2
O &
O 
 
! &P

.  
&

 %&
O
!
 %
!!


. >
G
'



 . 
&!&  
. 
O
%<(. >
G. 

%<(
G
G. 
 
. > !! 

2
 %&$  %

  5
2!%
! :AQ 


Obiekty klasy  bd wic budoway struktury przypominajce poniszy schemat:

Sama lista jednokierunkowa skada si z obiektw typu ), jest to jednak wycznie szczeg implementacyjny klasy , zatem struktur ) zadeklarowalimy jako prywatny typ tej klasy. Zauwa, e dla typu ) zdefiniowalimy
konstruktor zapewniajcy waciwe inicjalizowanie wszystkich pl tej struktury.
Twoje umiejtnoci umoliwiajce tworzenie kodu dla list jednokierunkowy z zamknitymi oczami nie mog Ci przesania korzyci pyncych z wykorzystania tego
typu konstruktorw.

Sposb 41. Rozrniaj dziedziczenie od stosowania szablonw

191

Oto pierwsza prba zaimplementowania funkcji skadowych klasy . Jak wikszo prototypowych implementacji (dalekich od ostatecznej, handlowej wersji oprogramowania), poniszy kod nie zawiera instrukcji wykrywajcych bdy, poniewa
w wiecie prototypw nic si nigdy nie psuje:
 8 
&!& %F
! %
 2
O &

 5
%.  &( !!
%


 !$
O 

. > E,5 !2 %&
!!!
 5 ?@
G
O5 E,?@!2 %&


 E,



L%%!
! 

%2 
. > <5 !&%F
 
!!!
 5 ?@
G!2 ! 
'
# 

 <% !

!!!


 


 558

Powysze implementacje nie zawieraj w sobie adnych nowych, fascynujcych elementw. W rzeczywistoci jedynym interesujcym szczegem w powyszym kodzie
jest to, e kada z zaprezentowanych funkcji skadowych zostaa opracowana bez najmniejszej znajomoci docelowego typu 2 (zakadamy jedynie, e moemy wywoywa
konstruktor kopiujcy typu 2, ale jak wynika z treci sposobu 45. mamy do tego
prawo). Stworzony kod konstruujcy i niszczcy stos, kadcy i zdejmujcy elementy
ze stosu oraz okrelajcy, czy stos jest pusty, jest cakowicie niezaleny od typu 2.
Wyjtkiem jest zaoenie, e moemy dla tego typu wywoywa konstruktor kopiujcy, jednak zachowanie obiektw zdefiniowanej powyej klasy w aden inny sposb
nie jest uzalenione od 2. Powyszy przykad doskonale obrazuje podstawow zasad
szablonw klas zachowanie klasy nie zaley od typu przetwarzanych obiektw.
Przeksztacenie klasy  w szablon jest zreszt tak proste, e mgby to zrobi kady:
MO@
  
   % !
 !


192

Dziedziczenie i projektowanie zorientowane obiektowo

Wrmy teraz do kotw. Dlaczego szablony nie bd dobrym rozwizaniem dla klas
reprezentujcych koty?
Przeczytaj raz jeszcze specyfikacj i zwr uwag na jedno z wymaga: kada rasa
kotw je i pi w charakterystyczny dla siebie, ujmujcy sposb. Oznacza to, e musimy zaimplementowa rne zachowania dla poszczeglnych typw (ras) kotw.
Nie moemy po prostu napisa jednej funkcji obsugujcej zachowania wszystkich
kotw, moemy jedynie stworzy specyfikacj interfejsu dla funkcji, ktr kady typ
kotw musi implementowa. Aha! Moemy przekaza wycznie interfejs funkcji,
deklarujc jedynie czyst funkcj wirtualn (patrz sposb 36.):
D

LD! :7R
 58%! &!$
 58%! 4$


Podklasy klasy # powiedzmy   i     32 musz


oczywicie ponownie zdefiniowa dziedziczone interfejsy funkcji  i :
D

 
 


+22 1OD

 
 



Dobrze, wiemy ju, dlaczego szablony s dobrym rozwizaniem dla klasy , i dlaczego nie powinnimy ich stosowa w przypadku klasy # . Wiemy take, dlaczego
waciwym rozwizaniem dla klasy # jest dziedziczenie. Pozostaje wic tylko pytanie,
dlaczego nie powinnimy stosowa dziedziczenia dla klasy . Aby sobie na to
pytanie odpowiedzie, sprbujmy zadeklarowa najwysz klas ( ) z hierarchii
klas reprezentujcych stosy klas bazow, po ktrej wszystkie pozostae klasy
reprezentujce stosy bd dziedziczyy:
 & %

 2
PPP &58
PPP 58



Wszystko jest ju jasne. Jaki typ powinnimy zadeklarowa dla czystych funkcji
wirtualnych   i ? Pamitaj, e kada podklasa musi ponownie zadeklarowa
dziedziczone funkcje wirtualne z dokadnie tymi samymi typami parametrw i typami
zwracanych wynikw, z ktrymi funkcje te zostay zadeklarowane w klasie bazowej.

Sposb 42. Dziedziczenie prywatne stosuj ostronie

193

Niestety, stos liczb cakowitych typu 


moe obsugiwa operacje kadzenia i zdejmowania tylko wartoci typu 
, a np. stos typu # moe obsugiwa operacje kadzenia i zdejmowania tylko obiektw klasy # . Jak wic moemy zadeklarowa
w klasie  jej czyste funkcje wirtualne w taki sposb, by klienci mogli tworzy
zarwno stosy liczb cakowitych, jak i stosy obiektw klasy # ? Gorzka prawda jest
taka, e nie moemy tego zrobi i wanie dlatego dziedziczenie nie jest waciwym
mechanizmem do tworzenia stosw.
By moe sdzisz, e jeste sprytniejszy. Moe Ci przyj do gowy, e jeste w stanie
przechytrzy swoje kompilatory, wykorzystujc oglne wskaniki (). Okazuje
si jednak, e w tym przypadku wskaniki typu  Ci nie pomog. Obejcie wymagania, by deklaracje wirtualnych funkcji w klasach potomnych byy zgodne z deklaracjami w klasie bazowej, jest zwyczajnie niemoliwe. Oglne wskaniki mog nam
jednak pomc w rozwizaniu zupenie innego problemu zwizanego z efektywnoci
klas generowanych na podstawie szablonw (szczegy znajdziesz w sposobie 42.).
Zakoczmy nasze rozwaania dotyczce stosw i kotw sprbujmy teraz podsumowa wnioski pynce z treci tego sposobu:
 Szablon powinien by wykorzystywany do generowania zbioru klas

w sytuacji, gdy typ przetwarzanych obiektw nie wpywa na zachowania


nalecych do definiowanej klasy funkcji.
 Dziedziczenie powinno by wykorzystywane dla zbioru klas w sytuacji,

gdy typ przetwarzanych obiektw wpywa na zachowania nalecych


do definiowanej klasy funkcji.
Pocz te dwa punkty, a poczynisz ogromny krok w kierunku mistrzostwa we waciwym dobieraniu mechanizmu dziedziczenia i szablonw.

Sposb 42.
Dziedziczenie prywatne stosuj ostronie
Sposb 42. Dziedziczenie prywatne stosuj ostronie

Prezentujc sposb 35., wykazaem, e C++ traktuje publiczne dziedziczenie jak relacj jest. Zademonstrowaem to na przykadzie sytuacji, w ktrej kompilatory, majc
hierarchi, w ktrej klasa 
publicznie dziedziczy po klasie  
, niejawnie
przeksztacaj obiekty klasy 
w obiekty klasy  
, gdy jest to niezbdne do
poprawnej realizacji wywoania funkcji. Warto w tym momencie przypomnie fragment tamtego przykadu z jedn zmian zamiast dziedziczenia publicznego zastosujemy dziedziczenie prywatne:

   

! &

   !!!
%

 


  !"
 

 
&$

194

Dziedziczenie i projektowanie zorientowane obiektowo



!
&  '

!
&


 !(!
&  '

)$*

&  $

Oczywisty wniosek jest taki, e dziedziczenie prywatne nie oznacza relacji jest. Co
wic faktycznie oznacza?
Mylisz pewnie, e zanim zajmiemy si rzeczywistym znaczeniem dziedziczenia
prywatnego, powinnimy przeanalizowa zachowanie programw, w ktrych wykorzystujemy ten mechanizm. Dobrze, pierwsz regu rzdzc prywatnym dziedziczeniem moglimy wanie zaobserwowa w przeciwiestwie do dziedziczenia
publicznego, kompilatory w oglnoci nie przeksztacaj obiektw klasy potomnej
(np. klasy 
) w obiekty klasy bazowej (np. klasy  
), jeli zdefiniowano
midzy tymi klasami relacj dziedziczenia prywatnego. Dlatego wanie wywoanie
funkcji 
 dla obiektu zakoczyo si niepowodzeniem. Druga regua okrela, e
skadowe odziedziczone po prywatnej klasie bazowej staj si prywatnymi skadowymi klasy potomnej, nawet jeli w klasie bazowej zostay zadeklarowane jako skadowe chronione lub publiczne. To wszystko, co moemy powiedzie o zachowaniu
kodu zawierajcego dziedziczenie prywatne.
Przejdmy wic do rzeczywistego znaczenia tej relacji. Dziedziczenie prywatne modeluje relacj implementacji z wykorzystaniem. Kiedy deklarujemy klas  prywatnie
dziedziczc po klasie , robimy to dlatego, e chcemy w klasie  wykorzysta cz
kodu napisanego dla klasy ; pojciowe relacje czce obiekty typu  z obiektami typu
 nie maj tutaj adnego znaczenia. Oznacza to, e dziedziczenie prywatne ma wycznie charakter techniki implementacji klas. Posugujc si jzykiem ze sposobu 36.,
moemy powiedzie, e dziedziczenie prywatne oznacza, e dziedziczona powinna by
tylko implementacja, interfejs powinien by cakowicie ignorowany. Jeli klasa 
dziedziczy prywatnie po klasie , oznacza to tylko tyle, e obiekty klasy  s implementowane z wykorzystaniem obiektw klasy . Dziedziczenie prywatne nie jest
technik wykorzystywan w fazie projektowania oprogramowania, a jedynie podczas
implementowania programw.
Fakt, e prywatne dziedziczenie oznacza w rzeczywistoci relacj implementacji
z wykorzystaniem jest nieco mylcy, poniewa, prezentujc sposb 40. stwierdziem,
e takie samo znaczenie ma rozmieszczanie obiektw w warstwach. Na jakiej podstawie powinnimy wic wybiera waciw technik z tej pary? Odpowied jest prosta stosuj podzia na warstwy zawsze, gdy jest to moliwe; stosuj dziedziczenie
prywatne tylko wtedy, gdy jest to jedyne rozwizanie. Kiedy moemy mie do czynienia z t drug sytuacj? Kiedy mamy do czynienia z chronionymi skadowymi
i (lub) wirtualnymi funkcjami.
W sposobie 41. opisaem sposb tworzenia szablonu , ktrego zadaniem byo
generowanie klas przechowujcych obiekty rnych typw. By moe powiniene
zapozna si teraz z treci tego sposobu. Szablony s jednym z najbardziej przydatnych elementw udostpnianych w jzyku C++, kiedy jednak zaczniesz je stosowa
regularnie, szybko odkryjesz, e tworzc wiele obiektw danego szablonu, prawdopodobnie zwielokrotniasz take jego kod. W przypadku szablonu  kod funkcji

Sposb 42. Dziedziczenie prywatne stosuj ostronie

195

skadowych klasy *


+ bdzie przecie zupenie inny ni kod funkcji skadowych klasy * +. W niektrych sytuacjach nie mona tego unikn, jednak
z podobn powtarzalnoci kodu mamy prawdopodobnie do czynienia nawet w przypadkach, gdy funkcje szablonu w rzeczywistoci mogyby wykorzystywa wsplny
kod. Wynikajcy z tego przyrost rozmiarw kodu obiektu ma swoj nazw spowodowane przez szablon puchnicie kodu. Nie jest to oczywicie pozytywne zjawisko.
W przypadku niektrych typw klas moesz unikn tego zjawiska, stosujc wskaniki oglne. Dotyczy to klas przechowujcych wskaniki zamiast obiektw i zaimplementowanych zgodnie z nastpujcymi reguami:
1. Pojedyncza klasa zawiera wskaniki typu  do obiektw.
2. Istnieje dodatkowy zbir klas, ktrych jedynym celem jest egzekwowanie

cisej kontroli typw. Wszystkie te klasy wykorzystuj do normalnego


dziaania ogln klas z punktu 1.
Oto przykad zastosowania klasy  ze sposobu 41. w wersji niebdcej szablonem; jedyna zmiana dotyczy przechowywania wskanikw oglnych zamiast przechowywanych wczeniej obiektw:
I


I

LI

 2 > &
 > 
 


. 
 >
!
 %
!!


. >
G
'


.  >
%<(. >
G. 

%<(
G
G. 
 
. > !! 
I

I
2
 %  %

I
!%
!
  5
I
2 :AQ 


Poniewa powysza klasa przechowuje wskaniki zamiast obiektw, istnieje moliwo, e dany obiekt jest wskazywany przez wicej ni jeden stos (zosta pooony na
wielu stosach). Zasadnicze znaczenie ma w takiej sytuacji zapewnienie, by funkcja
 i destruktor klasy nie usuway wskanika   w adnym niszczonym obiekcie
typu ) i jednoczenie nadal usuway sam obiekt ). Obiekty typu
) maj w kocu przydzielan pami wewntrz klasy '
 , zatem

196

Dziedziczenie i projektowanie zorientowane obiektowo

take tam musz by usuwane. Oznacza to, e implementacja klasy  ze sposobu
41. niemal w zupenoci wystarcza take dla klasy '
 . Jedyne potrzebne
zmiany to zastpienie typu 2 typem .
Sama klasa '
  jest mao uyteczna jest te zbyt prosta, by moliwe byo
jej niewaciwe wykorzystanie. Klient mgby jednak przez pomyk pooy na stosie przechowujcym wycznie wskaniki do liczb cakowitych typu 
wskanik do
obiektu klasy # , a kompilatory i tak zaakceptowayby takie posunicie. W kocu
parametr bdcy wskanikiem typu  moe dotyczy dowolnych obiektw.
Aby odzyska bezpieczestwo typw, do ktrego zdylimy si ju przyzwyczai,
musimy stworzy klasy interfejsu do '
 :
;
 
,&% 4


 2
>
  2


>  
NM
>@  
 
 
 

I

&

D 
,&% 4D

 2D>  2
D>  
NMD>@  
 
 
 

I

&


Jak wida, zadaniem klas 


 i #  jest wycznie zapewnienie cisej
kontroli typw. Pierwsza klasa umoliwia umieszczanie na stosie i zdejmowanie ze
stosu jedynie wartoci cakowitoliczbowych typu 
; druga zezwala na umieszczanie
na stosie i zdejmowanie ze stosu wycznie obiektw klasy # . Obie klasy (zarwno

, jak i # ) zostay zaimplementowane w powizaniu z klas '
&
 (t relacj wyraamy za pomoc mechanizmu podziau na warstwy patrz
sposb 40.) i obie klasy wykorzystuj kod napisany dla funkcji skadowych klasy
'
 , ktre w rzeczywistoci implementuje ich zachowania. Co wicej, fakt,
e wszystkie funkcje skadowe klas 
 i #  s (niejawnie) wbudowywane,
oznacza, e koszt wykorzystania powyszych klas interfejsw w czasie wykonywania
programu jest bliski zeru.
Co si jednak stanie, jeli potencjalni klienci nie zdadz sobie z tego sprawy? Co si
stanie, kiedy bdnie przyjm, e zastosowanie samej klasy '
  jest bardziej
efektywne lub jeli s na tyle lekkomylni, e sdz, i tworzenie kodu bezpiecznie
operujcego typami jest domen miczakw? Jak moemy zmusi ich do stosowania
poredniczcych klas 
 i # , zamiast bezporednich odwoa do skadowych klasy '
 , ktre mog prowadzi do tego rodzaju bdw typw,
ktrych starali si szczeglnie unikn twrcy C++?

Sposb 42. Dziedziczenie prywatne stosuj ostronie

197

Nic, nic nie moe zmusi do tego potencjalnych klientw Twojego kodu. A moe
jednak istnieje co, co moe pomc?
Na pocztku tego sposobu wspomniaem, e alternatywnym sposobem ustanawiania
pomidzy klasami relacji implementacji z wykorzystaniem jest wizanie ich dziedziczeniem prywatnym. W tym przypadku technika ta okazuje si lepsza ni dzielenie na
warstwy, poniewa umoliwia wyraanie zaoenia, e klasa '
  nie jest
wystarczajco bezpieczna dla oglnych zastosowa i powinna by wykorzystywana
wycznie w charakterze implementacji innych klas. Mona to wyrazi, umieszczajc
funkcje skadowe klasy '
  w bloku   :
I

 
I

LI

 2 > &
 > 
 


    %!4
&

I
)$*
 &2


;


 2
>
 I
2


>  
NM
>@I
 
 
 
I


D

 2D> I
2
D>  
NMD>@I
 
 
 
I


;
 !
D !

Podobnie jak rozwizanie oparte na podziale na warstwy, powysza implementacja


oparta na prywatnym dziedziczeniu pozwala unikn powielania kodu, poniewa
bezpieczne pod wzgldem obsugi typw klasy interfejsw skadaj si wycznie
z wbudowanych wywoa funkcji skadowych klasy '
  (bdcych waciw implementacj).
Budowa bezpiecznych pod wzgldem obsugi typw interfejsw ponad klas '
&
 jest sprytnym rozwizaniem, jednak rczne tworzenie klas interfejsw dla
wszystkich moliwych typw byoby bardzo pracochonne. Na szczcie nie musimy

198

Dziedziczenie i projektowanie zorientowane obiektowo

tego robi moemy przecie wykorzysta szablon, ktry wygeneruje potrzebne


klasy automatycznie. Oto oparty na prywatnym dziedziczeniu szablon generujcy
bezpiecznie operujce na typach interfejsy stosw:
MO@
I


 2O> & I
2 &
O>  
NMO>@I
 
 
 
I



By moe nie jest to dla Ciebie od razu takie oczywiste, jednak powyszy kod jest
niesamowity dziki zastosowaniu szablonu kompilatory automatycznie wygeneruj tyle klas interfejsw, ile bdziemy potrzebowa. Poniewa generowane klasy s
bezpieczne pod wzgldem obsugi typw, popeniane przez klienta bdy w tym zakresie bd wykrywane ju w czasie kompilacji. Poniewa funkcje skadowe klasy
'
  s chronione oraz poniewa klasy interfejsw wykorzystuj '
&
 jako prywatn klas bazow, klienci nie mog obej klas interfejsw i uzyska
bezporedniego dostpu do klasy implementacji. Poniewa kada funkcja skadowa
klasy interfejsu jest (niejawnie) deklarowana z atrybutem 

, stosowanie klas
bezpiecznie obsugujcych typy nie powoduje adnych dodatkowych kosztw w trakcie wykonywania programu wygenerowany kod jest identyczny jak kod obsugujcy bezporedni dostp do skadowych klasy '
  (zakadajc, e kompilatory uwzgldniaj dyrektywy 

 patrz sposb 33.). Poniewa w klasie
'
  zastosowalimy wskaniki , ponosimy koszty operowania na stosach przez dokadnie jedn kopi kodu, niezalenie od liczby rnych typw stosw
wykorzystywanych w programie. Krtko mwic, zaprezentowany projekt zapewnia
naszemu programowi maksymaln efektywno i maksymalne bezpieczestwo typw. Nieatwo bdzie skonstruowa lepsze rozwizanie.
Jednym z wnioskw pyncych z tej ksiki jest ten, e rne wasnoci jzyka C++
wzajemnie na siebie oddziaywaj niekiedy w sposb niezwyky. Myl, e zgodzisz
si ze mn, e powysze rozwizanie jest tego dobrym przykadem.
Nie moglibymy zrealizowa omawianego przykadu za pomoc mechanizmu podziau na warstwy. Wynika to z faktu, e tylko mechanizm dziedziczenia daje moliwo dostpu do chronionych skadowych i tylko dziedziczenie umoliwia ponowne
definiowanie wirtualnych funkcji (przykad funkcji wirtualnych, ktrych obecno
moe skoni programist do zastosowania prywatnego dziedziczenia, znajdziesz
w sposobie 43.). W sytuacjach, w ktrych mamy do czynienia z klas zawierajc
funkcje wirtualne i chronione skadowe prywatne, dziedziczenie jest niekiedy jedynym sposobem wyraania relacji implementacji z wykorzystaniem pomidzy klasami.
Nie powiniene wic obawia si stosowania dziedziczenia prywatnego, kiedy okae
si, e jest to najbardziej waciwa technika, jak masz w danym przypadku do dyspozycji. Powiniene jednak pamita, e lepsz technik jest w oglnoci dzielenie na
warstwy, powiniene wic stosowa j zawsze, kiedy moesz.

Sposb 43. Dziedziczenie wielobazowe stosuj ostronie

199

Sposb 43.
Dziedziczenie wielobazowe stosuj ostronie
Sposb 43. Dziedziczenie wielobazowe stosuj ostronie

Rni programici rozmaicie postrzegaj technik dziedziczenia wielobazowego


jedni sdz, e jest dzieem samego Boga, inni twierdz, e jest oczywistym dowodem na istnienie szatana.
Zwolennicy dziedziczenia wielobazowego utrzymuj, e technika ta jest niezwykle
istotnym elementem naturalnego modelowania problemw wiata rzeczywistego;
przeciwnicy przekonuj natomiast, e jest wolna, trudna w implementacji i nie daje
wikszych moliwoci ni zwyke dziedziczenie po pojedynczej klasie bazowej.
Niestety, take wiat obiektowych jzykw programowania jest w tym wzgldzie
podzielony dziedziczenie wielobazowe jest moliwe w jzyku C++, Eiffel
i Common LISP Object System (CLOS), nie jest dostpne w jzykach Smalltalk,
Objective C i Object Pascal, natomiast Java obsuguje t technik w ograniczonej
formie. W co biedny programista powinien wic wierzy?
Zanim uwierzysz w cokolwiek, powiniene uporzdkowa pewne fakty. Niezaprzeczaln cech dotyczc dziedziczenia wielobazowego w C++ jest fakt, e otwiera
puszk Pandory zawierajca mnstwo komplikacji, ktre zwyczajnie nie maj miejsca
w przypadku dziedziczenia zwykego. Najprostsz z nich jest wieloznaczno wywoa
dziedziczonych funkcji skadowych (patrz sposb 26.). Jeli klasa potomna dziedziczy
skadow o tej samej nazwie po wicej ni jednej klasie bazowej, kade odwoanie do
tej nazwy jest niejednoznaczne; musisz wic jawnie okrela, o ktr skadow Ci
chodzi. Oto przykad oparty na naszych rozwaaniach ze sposobu 50.:
J 


%


I2E&


%


J 
J (
I2E&
 &) %&%

J 
>5
%J 

?@%)$*
&
!
!
4"
?@J % !
?@I2E&% !

200

Dziedziczenie i projektowanie zorientowane obiektowo

Powysze wywoania funkcji  wygldaj dosy niezgrabnie, ale przynajmniej


dziaaj prawidowo. Niestety, taki wygld wywoa jest stosunkowo trudny do wyeliminowania. Niejednoznacznoci wywoa nie mona wyeliminowa, nawet definiujc jedn z dziedziczonych funkcji jako prywatn (a wic niedostpn) istnieje
sensowne wytumaczenie takiego zachowania; omwiem je w sposobie 26.
Jawne kwalifikowanie wywoywanych skadowych jest nie tylko niezgrabne, rodzi
take pewne ograniczenia. Kiedy jawnie kwalifikujemy dan funkcj wirtualn z nazw klasy, funkcja przestaje by traktowana jak wirtualna. Zamiast tego, wywoywana funkcja to dokadnie ta, ktr wyznaczamy; nawet jeli obiekt, dla ktrego jest
wywoywana, jest egzemplarzem klasy potomnej:
J 
J 



%


5
%J 

?@%)$*?

&
!
!
4"
?@J %%% )&J %
?@I2E&%%% )&I2E&%

Zauwa, e mimo i w tym przypadku  wskazuje na obiekt klasy 4 &


  
, nie mamy moliwoci (bez rzutowania w d hierarchii klas patrz sposb 39.) wywoania funkcji  zdefiniowanej w tej wanie klasie.
Poczekaj, jest co jeszcze. Zarwno wersja funkcji  z klasy 4 , jak i wersja
z klasy '5 zostaa zadeklarowana jako wirtualna po to, by podklasy
tych klas mogy je ponownie definiowa (patrz sposb 36.), co si jednak stanie, kiedy
sprbujemy w klasie 4    
zdefiniowa ponownie obie wersje? Niestety,
nie moemy tego zrobi, poniewa klasa moe zawiera tylko jedn bezargumentow
funkcj skadow nazwan  (istnieje szczeglny wyjtek od tej reguy, kiedy jedna z funkcji jest staa, a druga nie patrz sposb 21.).
Omawiany problem by uwaany za tak istotny, e rozwaano nawet wprowadzenie
odpowiednich zmian w jzyku C++. Chodzio o wprowadzenie moliwoci zmiany
nazw dziedziczonych funkcji wirtualnych, jednak szybko zdano sobie spraw, e
problem mona wyeliminowa dodajc par nowych klas:
BGJ J 


 <%58

% 
 <%

BGI2E&I2E&


#2E&<%58

% 
#2E&<%


Sposb 43. Dziedziczenie wielobazowe stosuj ostronie

201

J 
J (
I2E&


 <%

#2E&<%



Kada z dwch nowych klas, ! %4  i ! %'5 , deklaruje w istocie


now nazw dla dziedziczonej funkcji . Nowa nazwa przyjmuje posta czystej
funkcji wirtualnej (w tym przypadku, odpowiednio   i 5 &
), co sprawia, e konkretne podklasy musz je ponownie definiowa. Co wicej,
kada z klas ponownie definiujcych dziedziczon funkcj wywouje now czyst
funkcj wirtualn. W rezultacie wewntrz obu nowych klas nalecych do omawianej
hierarchii pojedyncza, niejednoznaczna nazwa funkcji  zostaa faktycznie rozbita
na dwie, jednoznaczne, ale funkcjonalnie rwnowane nazwy funkcji:  
i 5 :
J 
>5
%J 

J >5
I2E&># 5
%% )
,
&J 
J <%
?@%
%% )
,
&J 
#2E&<%
# ?@%

Powiniene dobrze zapamita powysz strategi, w ktrej sprytnie zastosowalimy


czyste funkcje wirtualne, proste funkcje wirtualne i funkcje z atrybutem 

 (patrz
sposb 33.). Po pierwsze, rozwizuje to problem, z ktrym moesz si pewnego dnia
spotka. Po drugie, przypomina o komplikacjach wynikajcych ze stosowania techniki
dziedziczenia wielobazowego. Tak, zaprezentowane rozwizanie dziaa poprawnie,
zastanw si jednak, czy naprawd chcesz wprowadza nowe klasy tylko po to, by
umoliwi sobie ponowne definiowanie wirtualnych funkcji. Klasy ! %4  i ! %&
'5 maj podstawowe znaczenie dla poprawnego funkcjonowania hierarchii, nie odpowiadaj jednak ani abstrakcji na poziomie definicji problemu, ani
abstrakcji na poziomie implementacji jego rozwizania. S tylko i wycznie narzdziem umoliwiajcym nam implementacj pewnego modelu. Wiesz ju, e dobre
oprogramowanie powinno by niezalene od tego typu narzdzi. Ta zasada ma zastosowanie take w tym przypadku.
Problem abstrakcji cho interesujcy moe znacznie ogranicza nasze moliwoci
wykorzystywania techniki dziedziczenia wielobazowego. Jak wynika z obserwacji,
kolejnym problemem jest fakt, e hierarchia dziedziczenia wielobazowego w postaci:
+   
D   
<+(D   

202

Dziedziczenie i projektowanie zorientowane obiektowo

wykazuje niepokojc tendencj do ewoluowania w kierunku hierarchii w postaci:


B   
+B   
DB   
<+(D   

Niezalenie od tego, czy prawd jest, e diamenty s najlepszymi przyjacimi kobiety, z pewnoci zaprezentowana powyej hierarchia dziedziczenia w ksztacie
diamentu nie jest przyjacielem programisty. Kiedy tworzymy podobn hierarchi, na
samym pocztku musimy sobie odpowiedzie na pytanie, czy ! powinna by wirtualn
klas bazow (czyli, czy dziedziczenie po tej klasie powinno by wirtualne). W praktyce odpowied niemal zawsze powinna by twierdzca; tylko w szczeglnych przypadkach bdziemy chcieli, by obiekt klasy  zawiera wiele kopii danych bdcych
skadowymi klasy !. W powyszym przykadzie w klasach  i # zadeklarowalimy !
jako wirtualn klas bazow.
Niestety, kiedy definiujemy klasy  i #, moemy nie wiedzie, czy jakakolwiek inna
klasa bdzie jednoczenie dziedziczya po nich obu i w rzeczywistoci do poprawnego
zdefiniowania tych klas taka wiedza nie powinna nam by potrzebna. Stawia nas to
w bardzo trudnym pooeniu, przynajmniej jako projektantw tych klas. Jeli nie zadeklarujemy ! jako wirtualnej klasy bazowej klas  i #, pniejsi projektanci klasy 
bd by moe zmuszeni do zmodyfikowania definicji klas  i #, by umoliwi sobie
ich efektywne wykorzystanie. Takie rozwizanie jest zazwyczaj nie do przyjcia,
zwykle dlatego, e definicje klas !,  i # s dostpne tylko do odczytu. Moe to wynika np. z faktu, e klasy !,  i # znajduj si w bibliotece, a klasa  jest tworzona
przez klienta tej biblioteki.
Z drugiej strony, jeli zadeklarujemy ! jako wirtualn klas bazow dla klas  i #,
bdziemy musieli zazwyczaj ponie dodatkowe koszty zarwno w wymiarze wykorzystywanej przestrzeni pamiciowej, jak i czasu dziaania programw klientw tych
klas. Wynika to z faktu, e wirtualne klasy bazowe s zwykle implementowane jako
wskaniki do obiektw, nie za jako same obiekty. Rozmieszczanie obiektw w pamici zaley zwykle od konkretnych dziaa poszczeglnych kompilatorw, jednak
faktem jest, e obiekt klasy  z niewirtualn klas bazow ! jest zazwyczaj umieszczany w szeregu przylegajcych komrek pamici, natomiast obiekt klasy  z wirtualn klas bazow ! jest niekiedy umieszczany w szeregu przylegajcych komrek
pamici, z ktrych dwa zawieraj wskaniki do komrek zawierajcych skadowe
z danymi wirtualnej klasy bazowej:

Sposb 43. Dziedziczenie wielobazowe stosuj ostronie

203

Nawet kompilatory niestosujce tej konkretnej strategii implementacji w oglnoci


nao na program klienta dodatkowy koszt zwizany ze zwikszonym wykorzystaniem pamici przez wirtualnie dziedziczce klasy.
Majc na uwadze powysz analiz, wyglda na to, e projektowanie efektywnych
klas wykorzystujcych technik dziedziczenia wielobazowego wymaga od projektantw bibliotek zdolnoci jasnowidztwa. Widzc, jak rzadk cech jest w naszych
czasach zdrowy rozsdek, przesadne poleganie na wasnociach jzyka, ktre wymagaj od projektantw nie tylko zwykego przewidywania przyszych zastosowa, ale
take zdolnoci wrbiarskich, jest bardzo ryzykowne.
To samo mona oczywicie powiedzie o wyborze pomidzy funkcjami wirtualnymi
a niewirtualnymi w klasie bazowej, istnieje jednak zasadnicza rnica. W sposobie 36.
wyjaniem, e funkcja wirtualna ma dokadnie zdefiniowane wysokopoziomowe
znaczenie, ktre jest inne od odpowiedniego, rwnie dokadnie zdefiniowanego wysokopoziomowego znaczenia funkcji niewirtualnej. Dokonanie waciwego wyboru
pomidzy tymi dwiema moliwociami jest wic moliwe w oparciu o to, co chcemy
przekaza autorom potencjalnych podklas. Podejmujc decyzj odnonie wirtualnej
lub niewirtualnej klasy bazowej nie mamy jednak do dyspozycji tak dobrze zdefiniowanych znacze wysokiego poziomu. Decyzj musimy wic opiera zwykle na strukturze caej hierarchii dziedziczenia, co oznacza, e odpowiednie kroki nie mog by
podejmowane do momentu jej zaprojektowania. Jeli musisz zna dokadne zastosowania swojej klasy, zanim przystpisz do jej poprawnego zdefiniowania, projektowanie efektywnych klas staje si bardzo trudne.
Kiedy ju poradzisz sobie z problemem niejednoznacznoci i odpowiesz na pytanie,
czy dziedziczenie po klasie bazowej (lub klas bazowych) powinno by wirtualne, nadal
czeka Ci wiele komplikacji. Zamiast nad nimi rozpacza, wspomn jedynie o dwch
problemach, na ktre powiniene zwraca szczegln uwag:
 Przekazywanie argumentw konstruktora do wirtualnych klas bazowych.

W przypadku zastosowania techniki niewirtualnego dziedziczenia argumenty


konstruktora klasy bazowej s wyznaczane za pomoc list inicjalizacji
skadowych klas poredniczcych w dziedziczeniu po klasie bazowej.

204

Dziedziczenie i projektowanie zorientowane obiektowo

Poniewa hierarchie pojedynczego dziedziczenia wymagaj wycznie


niewirtualnych klas bazowych, argumenty s przekazywane w gr hierarchii
dziedziczenia w sposb zupenie naturalny klasy na n-tym poziomie hierarchii
przekazuj argumenty do klas na poziomie (n 1). W przypadku konstruktorw
wirtualnej klasy bazowej argumenty s jednak wyznaczane za pomoc list
inicjalizacji skadowych klas najbardziej potomnych wzgldem klasy bazowej.
W efekcie klasa inicjalizujca wirtualn klas bazow moe by od niej dowolnie
oddalona w hierarchii dziedziczenia i moe si zmienia wraz z dodawaniem
do hierarchii nowych klas. Dobrym sposobem ominicia tego problemu
jest wyeliminowanie potrzeby przekazywania argumentw konstruktora
do wirtualnych klas bazowych. Najprostszym sposobem jest oczywicie unikanie
umieszczania danych skadowych w tych klasach. Przykadem takiego
rozwizania jest jzyk Java definiowane tak wirtualne klasy bazowe
(nazywane interfejsami) zwyczajnie nie mog zawiera adnych danych.
 Przewaga funkcji wirtualnych. Zaraz po tym, gdy stwierdzilimy,

e jestemy w stanie waciwie identyfikowa wszystkie niejednoznacznoci


stosowanych wywoa, zmieniy si istotne reguy ich zachowania.
Rozwamy ponownie przykad przypominajcego diament grafu dziedziczenia
dla klas !, , # i . Przypumy, e klasa ! definiuje wirtualn funkcj
skadow , ktra jest ponownie definiowana w klasie #, ale nie jest
ju definiowana w klasach  i :

Na podstawie wnioskw pyncych z naszych wczeniejszych analiz, wydawa


by si mogo, e bdziemy mieli do czynienia z niejednoznacznoci:
<>5
%<
?@,B,!D,P

Ktra wersja funkcji  powinna by wywoana dla obiektu klasy ?


Ta bezporednio dziedziczona po klasie #, czy te dziedziczona porednio
(przez klas ) po klasie !? Oto odpowied: to zaley od sposobu, w jaki klasy
 i  dziedzicz po klasie . W szczeglnoci, jeli ! jest niewirtualn klas
bazow dla klasy  lub #, przedstawione wywoanie jest niejednoznaczne;
jeli jednak ! jest wirtualn klas bazow zarwno dla klasy , jak i # mwimy,
e ponowna definicja funkcji  w klasie # dominuje nad oryginaln definicj
z klasy ! wywoanie funkcji  za porednictwem wskanika  bdzie
wwczas (jednoznacznie) dotyczyo wersji # . Jeli dokadnie przeanalizujesz
teraz to zachowanie, okae si, e wanie tego szukae, jednak dokadne
przeledzenie wszystkich aspektw tego zachowania moe by szalenie trudne.

Sposb 43. Dziedziczenie wielobazowe stosuj ostronie

205

By moe przyznasz teraz, e dziedziczenie wielobazowe moe prowadzi do wielu


komplikacji. By moe jeste przekonany, e nigdy nie bdziesz zmuszony wykorzysta
tej techniki dziedziczenia. By moe jeste gotowy zaproponowa midzynarodowej
komisji standaryzacji C++ usunicie dziedziczenia wielobazowego z tego jzyka lub
przynajmniej zaproponowa szefowi swojego projektu, by zakaza programistom stosowania tej techniki.
By moe jeste zbyt porywczy.
Pamitaj, e projektanci C++ nie stworzyli techniki dziedziczenia wielobazowego,
ktra byaby trudna w stosowaniu, dopiero pniej okazao si, e w poczeniu z innymi bardziej lub mniej sensownymi elementami ten typ dziedziczenia pociga
za sob pewne komplikacje. W powyszych rozwaaniach moge zauway, e
wikszo tych komplikacji pojawia si dopiero w poczeniu ze stosowaniem wirtualnych klas bazowych. Jeli wic moesz unikn ich stosowania (jeli moesz zrezygnowa z tworzenia morderczych grafw dziedziczenia), wikszo problemw po
prostu przestanie istnie.
Przykadowo, w sposobie 34. opisaem klas protokou istniejc wycznie po to, by
definiowa interfejs klasy potomnej omwiona klasa nie zawieraa adnych skadowych danych, nie definiowaa te adnych konstruktorw; zawieraa wycznie
wirtualny konstruktor (patrz sposb 14.) i penicy rol specyfikacji interfejsu zbir
czystych funkcji wirtualnych. Klasa protokou  
mogaby mie posta:



L


#

58

#2<
58

#
58

#


58


Klienci tej klasy musz wykorzystywa w swoich programach wskaniki i referencje


do  
, poniewa nie mona tworzy obiektw abstrakcyjnych klas.
Aby utworzy obiekt, ktry mona wykorzysta jak obiekty klasy  
, klienci tej
klasy musz wykorzysta specjalne funkcje fabryczne (patrz sposb 34.) tworzce
obiekty konkretnych podklas klasy  
:
,
&,!
% !$ 

 %


# 
, %!
2

>
<;<
;
,
<;<K- <;<
<;<5K- <;<

>5
% !  )#&$

,&

 &
>! 4
%
,
&) %2

%
 !
& 

206

Dziedziczenie i projektowanie zorientowane obiektowo

Jak jednak funkcja  


moe tworzy obiekty wskazywane przez zwracane
wskaniki? To proste, musi istnie jaka konkretna klasa potomna wzgldem klasy
 
, ktrej obiekty bd mogy by tworzone wewntrz funkcji  
.
Przypumy, e taka klasa nosi nazw " 
. Jako konkretna klasa, " 
musi
zapewnia implementacj dziedziczonych po klasie  
czystych funkcji wirtualnych. Mona je napisa od pocztku, jednak zgodnie z zaleceniami inynierii oprogramowania lepszym rozwizaniem bdzie wykorzystanie istniejcych komponentw,
ktrych wikszo lub wszyscy programici uywali ju w przeszoci. Przykadowo
zamy, e dla naszej starej bazy danych istnieje ju klasa  

, ktra zabezpiecza najwaniejsze potrzeby klasy " 
:

;
, 


;
, <;<
L
;
, 

2>2.


2>2+2<


2>2B


2>2.



2><E

!

2><D 

&



Moesz pomyle, e powysza klasa jest stara, poniewa jej funkcje skadowe zwracaj acuchy typu 
, zamiast obiektw typu 
. Jeli buty pasuj, dlaczego nie mielibymy ich nosi? Nazwy funkcji skadowych powyszej klasy sugeruj, e efekt prawdopodobnie bdzie dla nas satysfakcjonujcy.
Dochodzimy wreszcie do odkrycia, e klasa  

 zostaa jednak zaprojektowana po to, by uatwia wypisywanie z bazy danych pl z danymi w rnych formatach,
gdzie kade pole jest z gry i z dou ograniczone specjalnymi acuchami. Domylnymi
ogranicznikami otwierajcymi i zamykajcymi wartoci pl s nawiasy kwadratowe,
zatem warto pola lemur gruboogoniasty bdzie reprezentowana przez acuch:
SJ# #
T

Majc na uwadze fakt, e nawiasy kwadratowe nie s uniwersalnymi ogranicznikami


odpowiadajcymi wszystkim klientom klasy  

, wirtualne funkcje  &
5
i  #  umoliwiaj klasom potomnym wyznaczanie wasnych
acuchw penicych rol ogranicznikw otwierajcych i zamykajcych wartoci.
Implementacje nalecych do klasy  

 funkcji ),   , !&
 i ) 
  wywouj te wirtualne funkcje, by doda waciwe ograniczniki do zwracanych wartoci. Przykadowo, kod funkcji  

 ) moe
wyglda nastpujco:

2>
;
, <E




/S/ 4
 #
!
 %&$

Sposb 43. Dziedziczenie wielobazowe stosuj ostronie

207


2>
;
, <D 



/T/ 4
 #
!
!&$


2>
;
, 2.


% !, !%
&% 4
%&
!
(! 
!
&! %
!
2SCBUN-E0CBOO=<N-;=J<NVBJK=NJ=.IO1T
!& #
!
 %&$
(<E

  


 
 
!& #
!
!&$
(<D 



Mona oczywicie w powyszej definicji funkcji  



 ) doszukiwa
si wad (szczeglnie w zastosowaniu bufora o staym rozmiarze patrz sposb 23.),
powinnimy jednak odoy te rozwaania na bok i skupi si na czym innym
funkcja ) wywouje funkcj  5
celem wygenerowania ogranicznika otwierajcego zwracany acuch, nastpnie funkcja generuje sam warto reprezentujc nazwisko i wywouje funkcj  # . Poniewa  5

i  #  s funkcjami wirtualnymi, wynik zwracany przez funkcj )


jest uzaleniony nie tylko od definicji klasy  

, ale take od wszystkich klas
potomnych wzgldem tej klasy.
Dla programisty implementujcego klas " 
jest to dobra wiadomo, poniewa uwanie analizujc funkcje wypisujce z bazy danych wartoci klasy  
, odkrylimy, e zadaniem funkcji ) i jej siostrzanych funkcji skadowych jest
zwracanie nienaruszonych wartoci (pozbawionych ogranicznikw). Oznacza to, e
jeli dana osoba pochodzi z Madagaskaru, po wywoaniu dla tej osoby funkcji zwracajcej warto pola
 
  powinnimy otrzyma acuch 6" 6, a nie
67" 86.
Relacja czca klasy " 
i  

 polega na tym, e klasa  


zawiera niekiedy funkcje, dziki ktrym implementacja klasy " 
jest atwiejsza.
To wszystko, nie jest to wic relacja jest ani ma. Oznacza to, e musimy mie do
czynienia z relacj implementacji z wykorzystaniem, o ktrej wiemy, e moe by reprezentowana na dwa sposoby za pomoc podziau na warstwy (patrz sposb 40.)
lub za pomoc prywatnego dziedziczenia (patrz sposb 42.). W sposobie 42. stwierdziem, e technika podziau na warstwy jest w oglnoci lepszym rozwizaniem,
jednak w przypadku, gdy konieczne jest ponowne definiowane funkcji wirtualnych,
musimy zastosowa dziedziczenie prywatne. W omawianym przykadzie klasa "&

musi zawiera now definicj funkcji  5
i  # , zatem
zastosowanie podziau na warstwy jest niemoliwe " 
musi wic prywatnie
dziedziczy po klasie  

.

208

Dziedziczenie i projektowanie zorientowane obiektowo

Klasa " 
musi jednak take implementowa interfejs klasy  
, co wie si
z publicznym dziedziczeniem. Prowadzi nas to do ciekawego przykadu dziedziczenia
wielobazowego poczenia publicznego dziedziczenia interfejsu z prywatnym
dziedziczeniem implementacji:

 %!
!
,&(:
"
 %

L


#

58

#2<
58

#
58

#


58

<;<   % !%

&!!#:)
$

 


;
,  !%,
&!

 !
&


;
, <;<
L
;
, 

2>2.


2>2+2<


2>2B


2>2.



2><E

!

2><D 

&


C

(!%:"%#'


;
,  !!!
% ! %

C
<;<
;
, 

%
,
& !!!
2%
2,
& #
!
:%

2><E

 
//

2><D 
 
//

&%#
2,
&) %2


#


 

;
, 2.

#2<

 

;
, 2+2<

#

 

;
, 2B

#



 

;
, 2.



Sposb 43. Dziedziczenie wielobazowe stosuj ostronie

209

Graficznie mona to przedstawi nastpujco:

Powyszy przykad pokazuje, e technika dziedziczenia wielobazowego moe by


przydatna i zrozumiaa, chocia nieprzypadkowo nie mamy w tym przypadku do czynienia z przeraajcymi grafami dziedziczenia w ksztacie diamentw.
Nadal jednak musimy opiera si pokusie pochopnego stosowania dziedziczenia
wielobazowego. Moemy niekiedy wpa w puapk nieprzemylanego wykorzystania tej techniki do szybkiego poprawienia hierarchii dziedziczenia, ktra w rzeczywistoci wymaga gbszych zabiegw projektowych. Przykadowo, przypumy, e pracujemy nad hierarchi klas reprezentujcych postacie z animowanych kreskwek.
Przynajmniej na poziomie pojciowym sensownym rozwizaniem jest umoliwienie
kadej z postaci taczenia i piewania, jednak sposb realizacji tych czynnoci rni
si dla poszczeglnych bohaterw. Co wicej, domylnym zachowaniem podczas
piewania i taczenia jest brak jakichkolwiek dziaa.
Moemy to wyrazi w jzyku C++ w nastpujcy sposb:
D
D2

 

 
#


Naturalnym sposobem modelowania wymagania dotyczcego taczenia i piewania


wszystkich obiektw klasy # 
#  jest wykorzystanie funkcji wirtualnych.
Domylne zachowanie polegajce na braku operacji wyraamy za pomoc pustej
definicji tych funkcji wewntrz klas (patrz sposb 36.).
Przypumy, e jednym z konkretnych typw postaci w kreskwce jest konik polny,
ktry taczy i piewa w charakterystyczny dla siebie sposb:
I2 D
D2

 
,
&!
&&'#!4
!&
 
#,
&!
&&'#!4
!&


Przypumy teraz, e po zaimplementowaniu klasy '  decydujemy, e


bdziemy take potrzebowali klasy dla wierszczy:
DD
D2

 

 
#


210

Dziedziczenie i projektowanie zorientowane obiektowo

Kiedy zabierzesz si za implementowanie klasy # , uwiadomisz sobie, e moesz ponownie wykorzysta wikszo kodu napisanego wczeniej dla klasy ' &
. Kody funkcji nalecych do obu klas musz si jednak w paru szczegach
rni uwzgldniamy w ten sposb rnice pomidzy technikami taczenia i piewania konikw polnych i wierszczy. Nagle przychodzi nam do gowy sprytny sposb ponownego wykorzystania istniejcego kodu zaimplementujemy klas #
z wykorzystaniem klasy '  i wykorzystamy wirtualne funkcje, ktre umoliwi klasie # modyfikowanie zachowa z klasy ' !
Od razu powinnimy si zorientowa, e poczenie obu wymaga relacji implementacji z wykorzystaniem z moliwoci ponownego definiowania wirtualnych
funkcji oznacza, e klasa # musiaaby prywatnie dziedziczy po klasie ' &
, jednak wierszcz pozostaby oczywicie postaci z kreskwki, zatem klas
# musielibymy zdefiniowa w taki sposb, by dziedziczya zarwno po klasie
' , jak i # 
# :
DD
D2(
I2 

 

 
#


Dochodzimy teraz do momentu, w ktrym musimy wprowadzi niezbdne modyfikacje do klasy ' . W szczeglnoci musimy zadeklarowa kilka nowych wirtualnych funkcji, ktre bd ponownie definiowane w klasie # :
I2 D
D2

 

 
#
 
 
D !
7
 
D !
A
 
#D !



Taczenie konikw polnych moemy teraz zdefiniowa w nastpujcy sposb:


 I2 


  





D !
7
   







D !
A
   






Podobnie powinnimy zaimplementowa zachowanie konikw polnych podczas piewania.


Jest oczywiste, e musimy zaktualizowa klas # w taki sposb, by uwzgldniaa nowe wirtualne funkcje, ktre musi ponownie definiowa:

Sposb 43. Dziedziczenie wielobazowe stosuj ostronie

211

DD
D2(
I2 

 
 I2 

 
# I2 
#
 
 
D !
7
 
D !
A
 
#D !



Wyglda na to, e wszystko powinno dziaa prawidowo. Kiedy obiekt klasy #
ma zataczy, wykona wsplny kod funkcji 
 z klasy ' , wykona waciwy tylko do wierszczy kod funkcji 
 z klasy # , wykona kod funkcji
'  
 itd.
Zaprezentowany projekt zawiera jednak powan wad lepo dc do celu zamae bowiem zasad zwan brzytw Ockhama1. Ockhamizm gosi, e bytw nie naley mnoy bez koniecznoci, odrzuca tym samym wszelkie byty, do ktrych uznania
nie zmusza dowiadczenie. W tym przypadku tymi bytami s relacje dziedziczenia.
Jeli sdzisz, e dziedziczenie wielobazowe jest bardziej skomplikowane od zwykego
dziedziczenia (mam nadziej, e tak wanie sdzisz), zaproponowany projekt klasy
# jest niepotrzebnie tak skomplikowany.
Zasadniczy problem polega na tym, e nieprawd jest, e klasa # jest zaimplementowana z wykorzystaniem klasy ' . Klasy # i '  maj
po prostu troch wsplnego kodu. W szczeglnoci wykorzystuj wsplny kod definiujcy te zachowania podczas taczenia i piewania konikw polnych i wierszczy,
ktre dla obu typw postaci s identyczne.
Dziedziczenie jednej klasy po drugiej nie jest dobrym sposobem wyraania ich zalenoci polegajcej na wykorzystywaniu wsplnego kodu w takim przypadku obie
klasy powinny dziedziczy po jednej wsplnej klasie bazowej. Wsplny kod dla konikw polnych i wierszczy nie powinien nalee ani do klasy ' , ani do
klasy # ; powinien nalee do nowej klasy bazowej, po ktrej obie wymienione
klasy powinny dziedziczy, powiedzmy do klasy 
 :
D
D2   
;
D
D2

 
%:
 
:% 
2
 
#4%!!
 
 
D !
758
 
D !
A58
1

Zasada sformuowana przez redniowiecznego mnicha i teologa franciszkaskiego, angielskiego


przedstawiciela pnej scholastyki, Wilhelma Ockhama (waciwie William of Occam), 1300 1349
przyp. tum.

212

Dziedziczenie i projektowanie zorientowane obiektowo


 
#D !
58

I2 ;

 
 
D !
7
 
D !
A
 
#D !


D;

 
 
D !
7
 
D !
A
 
#D !



Zwr uwag na prostot tego projektu. Wykorzystujemy wycznie technik pojedynczego dziedziczenia. Co wicej, stosujemy wycznie dziedziczenie publiczne. Klasy
'  i # definiuj jedynie funkcje charakterystyczne dla reprezentowanych przez siebie postaci wsplny kod funkcji 
 i 
 dziedzicz po klasie

 . Wilhelm Ockham byby z nas dumny.
Mimo e nowy projekt jest prostszy od omawianego wczeniej wymagajcego dziedziczenia wielobazowego, moe pocztkowo robi wraenie bardziej skomplikowanego. W porwnaniu z wczeniejsz koncepcj (wykorzystujc technik dziedziczenia
wielobazowego), proponowana architektura z pojedynczym dziedziczeniem wie si
z koniecznoci wprowadzenia zupenie nowej klasy, ktra wczeniej nie bya konieczna. Po co wprowadza dodatkow klas, skoro nie jest potrzebna?
Ten przykad demonstruje uwodzicielski charakter techniki dziedziczenia wielobazowego. Dziedziczenie wielobazowe z zewntrz wyglda na atwiejsze nie wymaga
stosowania dodatkowych klas i chocia wie si z wywoywaniem kilku nowych
wirtualnych funkcji z klasy ' , nowe funkcje i tak musz zosta gdzie zdefiniowane.
Wyobramy sobie teraz programist konserwujcego wielk bibliotek klas C++, do
ktrej naley doda now klas (# ) do istniejcej hierarchii # 
# ' . Programista wie, e z istniejcej hierarchii korzysta mnstwo klientw,

Sposb 44. Mw to, o co czym naprawd mylisz...

213

zatem im wiksze zmiany wprowadzi do biblioteki, tym wiksze bdzie ich niezadowolenie. Programista stawia sobie jednak za cel zminimalizowanie tego zjawiska.
Dokadnie analizujc wszystkie moliwoci, dochodzi do wniosku, e jeli doda relacj pojedynczego dziedziczenia po klasie '  do nowej klasy # , hierarchia nie bdzie wymagaa adnych dodatkowych modyfikacji. Jego rado jest
uzasadniona udao mu si znaczco zwikszy funkcjonalno biblioteki kosztem
minimalnego zwikszenia jej zoonoci.
Wyobra sobie teraz, e to Ty jeste tym programist. Nie daj si wic skusi technice
dziedziczenia wielokrotnego.

Sposb 44.
Mw to, o co czym naprawd mylisz.
Zdawaj sobie spraw z tego, co mwisz
Sposb 44. Mw to, o co czym naprawd mylisz...

We wstpie do tej czci, powiconej dziedziczeniu i projektowaniu zorientowanemu


obiektowo, podkreliem wag waciwego zrozumienia, co poszczeglne konstrukcje
obiektowe jzyka C++ naprawd oznaczaj. Nie chodzi tylko o zwyk znajomo regu tego jzyka programowania. Przykadowo, reguy dla C++ mwi, e jeli klasa 
publicznie dziedziczy po klasie , istnieje standardowa konwersja ze wskanika do
obiektu klasy  do wskanika do obiektu klasy ; publiczne funkcje skadowe klasy 
s dziedziczone jako publiczne funkcje skadowe klasy  itd. Wszystkie te cechy s
oczywicie prawdziwe, jednak ta wiedza jest niemal bezuyteczna, kiedy prbujemy
przeoy nasz projekt na kod w C++. Musimy wic zda sobie spraw z faktu, e
publiczne dziedziczenie w rzeczywistoci oznacza relacj jest jeli klasa 
publicznie dziedziczy po klasie , kady obiekt klasy  jest take obiektem klasy .
Jeli wic w swoim projekcie wprowadzasz relacj jest, wiesz, e w implementacji
powiniene zastosowa dziedziczenie publiczne.
Waciwe okrelenie, co mamy na myli, jest jednak dopiero poow sukcesu. Drug,
rwnie wan poow stanowi waciwe rozumienie efektw naszych decyzji projektowych. Przykadowo, deklarowanie niewirtualnych funkcji przed przeanalizowaniem
zwizanych z tym ogranicze dla podklas jest nieodpowiedzialne, jeli nie cakowicie
niemoralne. Deklarujc niewirtualn funkcj, w rzeczywistoci sygnalizujesz, e dana
funkcja reprezentuje dziaanie niezalene od specjalizacji jeli nie zdajesz sobie
z tego sprawy, efekt moe by katastrofalny.
Rwnowanoci publicznego dziedziczenia i relacji jest oraz niewirtualnych funkcji
skadowych i niezalenoci od specjalizacji s przykadami sposobu, w jaki konkretne
konstrukcje jzyka C++ odpowiadaj rozwizaniom na poziomie projektu. Ponisza
lista jest podsumowaniem najwaniejszych odpowiednioci tego typu:
 Wsplna klasa bazowa oznacza wsplne cechy klas potomnych.
Jeli zarwno klasa 9, jak i klasa : deklaruje  jako swoj klas bazow,
klasy 9 i : dziedzicz wsplne dane skadowe i (lub) wsplne funkcje
skadowe po klasie  (patrz sposb 43.).

214

Dziedziczenie i projektowanie zorientowane obiektowo


 Publiczne dziedziczenie jest rwnowane z relacj jest. Jeli klasa 
publicznie dziedziczy po klasie , kady obiekt typu  jest take obiektem
typu , ale nie na odwrt (patrz sposb 35.).
 Prywatne dziedziczenie jest rwnowane z relacj implementacji
z wykorzystaniem. Jeli klasa  prywatnie dziedziczy po klasie , obiekty typu
 s po prostu implementowane z wykorzystaniem obiektw typu ; pomidzy
obiektami klasy  i  nie istnieje adna relacja pojciowa (patrz sposb 42.).
 Podzia na warstwy jest rwnowany z relacj implementacji
z wykorzystaniem. Jeli klasa ! zawiera skadow dan typu ,
obiekty typu ! albo zawieraj elementy typu , albo s zaimplementowane
z wykorzystaniem obiektw typu  (patrz sposb 40.).

Ponisze stwierdzenia dotycz sytuacji, w ktrych wykorzystywana jest technika publicznego dziedziczenia:
 Istnienie w klasie czystej funkcji wirtualnej oznacza, e dziedziczony
bdzie wycznie interfejs tej klasy. Jeli klasa # deklaruje czyst funkcj
wirtualn , podklasy klasy # musz dziedziczy interfejs tej funkcji,
a konkretne podklasy klasy # musz dostarczy wasn implementacj
funkcji  (patrz sposb 36.).
 Deklaracja prostej funkcji wirtualnej oznacza, e dziedziczony bdzie

zarwno interfejs tej funkcji, jak i jej domylna implementacja.


Jeli klasa # deklaruje prost (nie czyst) funkcj wirtualn , podklasy
klasy # musz dziedziczy interfejs tej funkcji, mog take jeli jest to
korzystne dziedziczy jej domyln implementacj (patrz sposb 36.).
 Deklaracja niewirtualnej funkcji oznacza, e dziedziczony bdzie

zarwno interfejs tej funkcji, jak i jej wymagana implementacja.


Jeli klasa # deklaruje prost (nie czyst) funkcj wirtualn , podklasy
klasy # musz dziedziczy zarwno interfejs tej funkcji, jak i jej
implementacj. Oznacza to, e zachowanie funkcji  jest niezalene
od specjalizacji (patrz sposb 36.).

You might also like