Professional Documents
Culture Documents
Kurs C++ PDF
Kurs C++ PDF
99
), a czsto take nagwki innych zainstalowanych bibliotek.
Chcc przejrze lub zmodyfikowa list katalogw z plikami doczanymi w Visual
C++ .NET, musimy wybra z menu Tools pozycj Options. Dalej przechodzimy do
zakadki Projects|VC++ Directories, a na licie rozwijalnej Show directories for:
wybieramy Include files.
Z cudzysowami
Drugi typ instrukcji #include wyglda nastpujco:
#include "nazwa_pliku"
Z nimtake zdylimy si ju spotka - stosowalimy go do wczania wasnych plikw
nagwkowych do swoich moduw.
Ten wariant #include dziaa w sposb nieco bardziej kompleksowy ni poprzedni.
Wpierw bowiem przeszukuje on biecy katalog - tzn. ten katalog, w ktrym
umieszczono plik zawierajcy dyrektyw #include. Jeli tam nie znajdzie podanego
pliku, wwczas zaczyna zachowywa si tak, jak #include z nawiasami ostrymi.
Przeglda wic zawarto katalogw z listy folderw plikw doczanych.
Ktry wybra?
Dwa rodzaje jednej dyrektywy to cakiem sporo. Ktr wybra w konkretnej sytuacji?
Nasz czy biblioteczny
Decyzja jest jednak bardzo prosta:
jeeli doczamy nasz wasny plik nagwkowy - taki, ktry znajduje si gdzie
blisko, na przykad w tym samym katalogu - to powinnimy skorzysta z
dyrektywy #include, podajc nazw pliku w cudzysowach
jeli natomiast wykorzystujemy nagwek biblioteczny, pochodzcy od
kompilatora czy innych zwizanych z nim komponentw - stosujmy #include z
nawiasami ostrymi
Teoretycznie mona byc zawsze stosowa wariant z cudzysowami. To jednak obniaoby
czytelnoc kodu, gdy nie mona byoby atwo odrni, ktre dyrektywy doczaj nasze
wasne nagwki, a ktre - nagwki biblioteczne. Lepiej wic stosowa rozrznienie.
Nie pisaem tego na pocztku tej sekcji, ale chyba wiesz doskonale (bo mwiem o tym
wczeniej), e poprawne jest doczanie wycznie plikw nagwkowych. S to pliki
zawierajce deklaracje (prototypy) funkcji nie-inline, definicje funkcji inline, deklaracje
99
Te nagwki sa niezalecane, naley stosowa ich odpowiedniki bez rozszerzenia .h i literk c na pocztku.
Zamiast np. math.h uywamy wic cmath.
Preprocesor 333
zapowiadajce zmiennych oraz definicje klas (a czsto take definicje szablonw, ale o
tym pniej). Pliki te maj zwykle rozszerzenie .h, .hh, .hxx lub .hpp.
cieki wzgldne
W obu wersjach #include moemy wykorzystywa tzw. cieki wzgldne (ang. relative
paths), cho prawdziwie przydatne s one tylko w dyrektywie z cudzysowami.
cieki wzgldne pozwalaj docza pliki znajdujce si w innym katalogu ni biecy
100
:
w podkatalogach lub w nadkatalogu czy te w innych katalogach tego samego poziomu.
Oto kilka przykadw:
#include "gui\buttons.h" // 1
#include "..\base.h" // 2
#include "..\common\pointers.hpp" // 3
Dyrektywa 1 powoduje doczenie pliku buttons.h z podkatalogu gui. Kolejne uycie
#include doczy nam plik base.h z katalogu nadrzdnego wzgldem obecnego. Z kolei
ostatnia dyrektywa powoduje wpierw wyjcie z aktualnego katalogu (..), nastpnie
wejcie do podkatalogu common, pobranie ze zawartoci pliku pointers.hpp i wstawienie
w miejsce linijki 3.
Jak wida, w #include mona wykorzysta te same zasady tworzenia cieek
wzgldnych, jakie obowizuj w caym systemie operacyjnych
101
.
Zabezpieczenie przed wielokrotnym doczaniem
Dyrektywa #include jest gupia jak cay preprocesor. Ona tylko wstawia tekst podanego
w pliku w miejsce swego wystpienia. Nie dba przy tym, czy takie wstawienie spowoduje
jakie niepodane efekty. A atwo moe przecie takie skutki wywoa
Wyobramy sobie, e doczamy plik A, ktry sam docza plik B i X. Niech plik B te
docza plik X i ju mamy problem: ewentualne definicje zawarte w X bd przez
kompilator odczytane dwukrotnie. Zareaguje on wtedy bdem.
Trzeba wic podj ku temu jaki rodki zaradcze.
Tradycyjne rozwizanie
Rozwizanie problemu znanym jeszcze z C jest zastosowanie kompilacji warunkowej.
Musimy po prostu obja cay plik nagwkowy (nazwijmy go plik.h) w dyrektywy
#ifndef-#endif:
#ifndef _PLIK__H_
#define _PLIK__H_
// (caa tre pliku nagwkowego)
#endif
Uyte tu makro (_PLIK__H_) powinno by najlepiej spreparowane w jaki sposb z nazwy
i rozszerzenia pliku - a jeli trzeba, take i ze cieki do niego.
100
Biecy - to znaczy ten katalog, gdzie znajduje sie plik z dyrektyw #include "...".
101
Jako separatora moemy uy slasha lub backslasha. Slash ma t zalet, e dziaa take w systemach
unixowych - jeli oczywicie dla kogo jest to zalet
Zaawansowane C++ 334
Jak to dziaa? Ot dyrektywa #ifndef przepuci tylko jedno wstawienie treci pliku. Przy
powtrnej prbie makro _PLIK__H_ bedzie ju zdefiniowane, wic caa zawarto pliku
zostanie wyczona z kompilacji.
Pomaga kompilator
Zaprezentowany wyej sposb ma przynajmniej kilka wad:
wymaga wymylania nazwy dla makra kontrolnego, co przy duych projektach,
gdzie atwo wystpuj nagwki o tych samych nazwach, staje si kopotliwe.
Sytuacja wyglda jeszcze gorzej w przypadku bibliotek pisanych przez nas: tam
makra powinni mie w nazwie take okrelenie biblioteki, aby nie prowokowa
potencjalnych konfliktw z innymi zasobami kodu
umieszczona na kocu pliku dyrektywa #endif moe by atwo przeoczona i
omykowo skasowana. Nietrudno te napisa jaki kod poza klamr #ifndef-
#else - on nie bdzie ju objty ochron
sztuczka wymaga a trzech linii kodu, w tym jednej umieszczonej na samym
kocu pliku
Mnie osobicie rozwizanie to wydaje si po prostu nieeleganckie - zwaszcza, e coraz
wicej kompilatorw oferuje inny sposb. Jest nim umieszczenie gdziekolwiek w pliku
dyrektywy:
#pragma once
Jest to wprawdzie polecenie zalene od kompilatora, ale obsugiwane przez wszystkie
liczce si narzdzia (w tym take Visual C++ .NET oraz kompilator GCC z Dev-C++).
Jest te cakiem prawdopodobne, e taka metoda rozwizania problemu wielokrotnego
doczania znajdzie si w kocu w standardzie C++.
Polecenia zalene od kompilatora
Na koniec omwimy sobie takie polecenia, ktrych wykonanie jest zalene od
kompilatora, jakiego uywamy.
Dyrektywa #pragma
Do wydawania tego typu polece suy dyrektywa #pragma:
#pragma polecenie
To, czy dane polecenie zostanie faktycznie wzite pod uwage podczas kompilacji, zaley
od posiadanego przez nas kompilatora. Preprocesor zachowuje si jednak bardzo
porzdnie: jeli stwierdzi, e dana komenda jest nieznana kompilatorowi, wwczas caa
dyrektywa zostanie po prostu zignorowana. Niektre troskliwe kompilatory wywietlaj
ostrzeenie o tym fakcie.
Po opis polece, jakie s dostpne w dyrektywie #pragma, musisz uda si do
dokumentacji swojego kompilatora.
Waniejsze parametry #pragma w Visual C++ .NET
Uywajcy innego kompilatora ni Visual C++ .NET mog opuci ten paragraf.
Poniewa zakadam, e wikszo czytelnikw uywa zalecanego na samym pocztku
kursu kompilatora Visual C++ .NET, sdz, e poyteczne bdzie przyjrzenie si kilku
parametrom dyrektywy #pragma, jakie s tam dostpne.
Preprocesor 335
Nie omwimy ich wszystkich, gdy nie jest to podrcznik VC++, a poza tym wiele z nich
dotyczy sprawa bardzo niskopoziomowych. Przypatrzymy si aczkolwiek tym, ktre mog
by przydatne przecitnemu programicie.
Opisy wszystkich parametrw dyrektywy #pragma w Visual C++ .NET moesz rzecz jasna
znale w dokumentacji MSDN.
Wybrane parametry podzieliem na kilka grup.
Komunikaty kompilacji
Pierwsza trjka parametrw #pragma pozwala na wywietlanie pewnych informacji
podczas procesu kompilacji programu. W przeciwiestwie do #error, polcenia nie
powoduje jednak przerwania tego procesu, lecz tylko peni funkcj powiadamiajc np.
o pewnych decyzjach podjtych w czasie kompilacji warunkowej.
Przyjrzyjmy si tym komendom.
message
Skadnia polecenia message jest nastpujca:
#pragma message("komunikat")
Gdy preprocesor napotka powysz linijk kodu, to wywietli w oknie komunikatw
kompilatora (tam, gdzie zwykle podawane s bdy) wpisany tutaj komunikat. Jego
wypisanie nie spowoduje jednak przerwania procesu kompilacji, co rni #pragma
message od dyrektywy #error.
Przykadowym uyciem tego polecenie moe by pitrowy #if podobny do tego z jakim
mielimy do czynienia w poprzednim podrozdziale:
#define KEYBOARD 1
#define MOUSE 2
#define TRACKBALL 3
#define JOYSTICK 4
#define INPUT_DEVICE KEYBOARD
#if (INPUT_DEVICE == KEYBOARD)
#pragma message("Wkompilowuje obsluge klawiatury")
#elif (INPUT_DEVICE == MOUSE)
#pragma message("Domylsne urzadzenie: mysz")
#elif (INPUT_DEVICE == TRACKBALL)
#pragma message("Sterowanie trackballem")
#elif (INPUTDEVICE == JOYSTICK)
#pragma message("Obsluga joysticka")
#else
#error "Nierozpoznane urzadzenie wejsciowe!"
#endif
Teraz, w zalenie od wartoci makra INPUT_DEVICE w polu komunikatw kompilatora
zobaczymy na przykad:
Sterowanie trackballem
W parametrze message moemy te stosowa makra, np.:
#pragma message("Kompiluje modul " __FILE__ ", ktory byl ostatnio " \
Zaawansowane C++ 336
"zmodyfikowany: " __TIMESTAMP__)
W ten sposb zobaczymy oprcz nazwy kompilowanego pliku take dat i czas jego
ostatniej modyfikacji.
deprecated
Nieco inne zastosowanie ma parametr deprecated, lecz take suy do pokazywania
komunikatw dla programisty podczas kompilacji. Oto jego skadnia:
#pragma deprecated(nazwa_1 [, nazwa_2, ...])
deprecated znaczy dosownie potpiony i jest to troch zbyt teatralna, ale adekwatna
nazwa dla tego parametru dyrektywy #pragma. deprecated pozwala na wskazanie, ktre
nazwy w programie (funkcji, zmiennych, klas, itp.) s przestarzae i nie powinny by
uywane. Jeeli zostan one wykorzystane w kodzie, wwczas kompilator wygeneruje
ostrzeenie.
Spjrzmy na ten przykad:
// ta funkcja jest przestarzaa
void Funkcja()
{
std::cout << "Mam juz dluga, biala brode...";
}
#pragma deprecated(Funkcja)
int main()
{
Funkcja(); // spowoduje ostrzeenie
}
W powyszym przypadku zobaczymy ostrzeenie w rodzaju:
warning C4995: 'Funkcja': name was marked as #pragma deprecated
Zauwamy, e dyrektyw #pragma deprecated umieszczamy po definicji przestarzaego
symbolu. W przeciwnym razie sama definicja spowodowaaby wygenerowanie
ostrzeenia.
Innym sposobem oznaczenia symbolu jako przestarzay jest poprzedzenie jego deklaracji
fraz __declspec(deprecated).
Moemy te oznacza makra jako przestarzae, lecz aby unikn ich rozwinicia w
dyrektywie #pragma, naley ujmowa nazwy makr w cudzysowy.
warning
Ten parametr nie generuje wprawdzie adnych komunikatw, ale pozwala na
sprawowanie kontroli nad tym, jakie ostrzeenia s generowae przez kompilator.
Oto skadnia dyrektywy #pragma warning:
#pragma warning(specyfikator_1: numer_1_1 [numer_1_2 ...] \
[; specyfikator_2: numer_2_1 [numer_2_2 ...]])
Wyglda ona do skomplikowanie, ale w praktyce stosuje si tylko jeden specyfikator
na kade uycie dyrektywy, wic waciwa posta staje si prostsza.
Preprocesor 337
Co dokadnie robi #pragma warning? Ot pozwala ona zmieni sposb traktowania przez
kompilator ostrzee o podanych numerach. Podejmowane dziaania okrela dokadnie
specyfikator:
specyfikator znaczenie
disable
Powoduje wyczenie raportowania podanych numerw ostrzee.
Sytuacje, w ktrych powinny wystpi, zostan po prostu zignorowane, a
programista nie bdzie o nich powiadamiany.
once
Sprawia, e podane ostrzeenia bd wywietlane tylko raz, przy
pierwszym wystpieniu powodujcych je sytuacji.
default
Przywraca sposb obsugi ostrzee do trybu domylnego.
error
Sprawia, e podane ostrzeenia bd traktowane jako bedy. Ich
wystpienie spowoduje wic przerwanie kompilacji.
1
2
3
4
Zmienia tzw. poziom ostrzeenia (ang. warning level). Generalnie wyszy
poziom oznacza mniejsz dolegliwo i niebezpieczestwo ostrzeenia.
Przesunicie danego ostrzeenia do okrelonego poziomu powoduje, e
jego interpretacja (wywietlanie, przerwanie kompilacji, itd.) zalee
bdzie od ustawie kompilatora dla danego poziomu ostrzee. Za
ustawienia te nie odpowiada jednak #pragma warning.
Tabela 15. Specyfikatory kontroli ostrzee dyrektywy #pragma warning w Visual C++ .NET
Skd natomiast wzi numer ostrzeenia? Jest on podawany w komunikacie
kompilatora - jest to liczba poprzedzona liter C, np.:
warning C4101: 'nZmienna' : unreferenced local variable
Do #pragma warning podajemy numer ju bez tej litery. Chcc wic wyczy powysze
ostrzeenie, stosujemy dyrektyw:
#pragma warning(disable: 4101)
Pamitajmy, e stosuje si on do wszystkich instrukcji po swoim wystpieniu - podobnie
jak wszystkie inne dyrektywy preprocesora.
Uwaga: jakkolwiek wyczanie ostrzee jest czasem konieczne, nie naley z tym
przesadza. Przede wszystkim nie wyczajmy wszystkich pojawiajcych si ostrzee
jak leci, lecz wpierw przyjrzyjmy si, jakie kod je powoduje. Kade uycie #pragma
warning(disable: numer) powinno by bowiem dokadnie przemylane.
Funkcje inline
Z poznanymi w tym rozdziale funkcjami inline jest zwizanych kilka parametrw
dyrektywy #pragma. Zobaczmy je.
auto_inline
#pragma auto_inline ma bardzo prost posta:
#pragma auto_inline([on/off])
Parametr ten kontroluje automatyczne rozwijanie krtkich funkcji przez kompilator. Ze
wzgldw optymalizacyjnych niektre funkcje mog by bowiem traktowane jako inline
nawet wtedy,gdy nie s zadeklarowane z przydomkiem inline.
Jeli z jakich powodw nie chcemy aby tak byo, moemy to wyczy:
#pragma auto_inline(off)
Zaawansowane C++ 338
Wszystkie nastpujce dalej funkcje na pewno nie bd rozwijane w miejscu wywoania -
chyba e sami tego sobie yczymy, deklarujc je jako inline.
Typowo #pragma auto_inline stosujemy dla pojedynczej funkcji w ten sposb:
#pragma auto_inline(off)
void Funkcja(/* ... */)
{
// ...
}
#pragma auto_inline(on)
Jeeli nie podamy w dyrektywie ani on, ani off, to stan auto_inline zostanie
zamieniony na przeciwny (z on na off lub odwrotnie).
inline_recursion
Ta komenda jest take przecznikiem:
#pragma inline_recursion([on/off])
Kontroluje ona rozwijanie wywoa rekurencyjnych (ang. recursive calls) w funkcjach
typu inline. Rekurencj (ang. recurrency) nazywamy zjawisko, kiedy jaka funkcja
wywouje sam siebie - oczywicie nie zawsze, lecz w zalenoci od spenienia jakich
warunkw. Wywoania rekurencyjne s prostym sposobem na tworzenie pewnych
algorytmw - szczeglnie takich, ktre operuj na rekurencyjnych strukturach danych,
jak drzewa. Rekurencja moe by bezporednia - gdy funkcja sama wywouje siebie - lub
porednia - jeli robi to inna funkcja, wywoana wczeniej przez t nasz.
Rekurencyjne mog by take funkcje inline. W takim wypadku kompilator domylnie
rozwija tylko ich pierwsze wywoanie; dalsze wywoania rekurencyjne s ju dokonywane
w sposb waciwy dla normalnych funkcji.
Mona to zmieni, powodujc rozwijanie take dalszych przywoa rekurencyjnych (w
ograniczonym zakresie oczywicie) - naley wprowadzi do kodu dyrektyw:
#pragma inline_recursion(on)
atwo si domysli, e inline_recursion jest domylnie ustawiona na off.
inline_depth
Z poprzednim poleceniem zwizane jest take to - dyrektywa #pragma inline_depth:
#pragma inline_depth(gboko)
gboko moe tu by sta cakowit z zakresu od zera do 255. Liczba ta precyzuje, jak
gboko kompilator ma rozwija rekurencyjne wywoania funkcji inline. Naturalnie,
wartoc ta ma jakiekolwiek znaczenie tylko wtedy, gdy ustawimy inline_recursion na
on. Ponadto warto 255 oznacza rozwijanie rekurencji bez ogranicze (z wyjtkiem rzecz
jasna zasobw dostpnych dla kompilatora).
Domylnie rozwijanych jest osiem rekurencyjnych wywoa inline. Pamitajmy, e
przesada z t wartoci moe do atwo doprowadzai do rozrostu kody wynikowego -
zwaszcza, jeli przesadzamy te z obdzielaniem funkcji modyfikatorami inline (a
szczeglnie __forceinline).
Preprocesor 339
Inne
Oto dwie ostatnie komendy #pragma w Visual C++, jednak wcale nie s one najmniej
wane. Jakby to powiedzieli Anglicy, one s last but not least :) Przyjrzymy si im.
comment
To polecenie umoliwa zapisanie pewnych informacji w wynikowym pliku EXE:
#pragma comment(typ_komentarza [, "komentarz"])
Umieszczone tak komentarze nie su naturalnie tylko do dekoracji (cho niektre do
tego te :D), lecz moga nie take dane wane dla kompilatora czy linkera. Wszystko
zaley od frazy typ_komentarza. Oto dopuszczalne moliwoci:
typ komentarza znaczenie
exestr
Umieszcza w skompilowanym pliku tekstowy komentarz, ktry linker
w niezmienionej postaci przenosi do konsolidowanego pliku EXE.
Napis ten nie jest adowany do pamici podczas uruchamiania
programu, niemniej istnieje w pliku wykonywalnym i mona go
odczyta specjalnymi aplikacjami.
user
Wstawia do skompilowanego pliku podany komentarz, jednak linker
ignoruje go i nie pojawia si on w wynikowym EXEku. Istnieje
natomiast w skompilowanym pliku .obj.
compiler
Dodaje do skompilowanego modulu informacj o wersji kompilatora.
Nie pojawia si ona wynikowym pliku wykonywalnym z programem.
Przy stosowaniu tego typu, nie naley podawa adnego komentarza,
bo w przeciwnym razie kompilator uraczy nas ostrzeeniem.
lib
Ten typ pozwala na podanie nazwy pliku statycznej biblioteki
(ang. static library), ktra bdzie linkowana razem ze
skompilowanymi moduami naszej aplikacji. Linkowanie dodatkowych
bibliotek jest czsto potrzebne, aby skorzysta z niestandardowego
kodu, np. Windows API, DirectX i innych.
linker
Tak moemy poda dodatkowe opcje dla linkera, niezalenie od tych
podanych w ustawieniach projektu.
Tabela 16. Typy komentarzy w dyrektywie #pragma comment w Visual C++ .NET
Spord tych moliwoci najczciej stosowane s lib i linker, poniewa pozwalaj
zarzdza procesem linkowania. Oprcz tego exestr umoliwia zostawienie w pliku EXE
dodatkowego tekstu informacyjnego, np.:
#pragma comment(exestr, "Skompilowano: " __DATE__ __TIME__)
Jak wida na zaczonym obrazku, w takim tekcie mona stosowa te makra.
once
Na ostatku przypomnimy sobie pierwsze poznane polecenie #pragma - once:
#pragma once
Wiemy ju doskonale, jakie jest dziaanie dyrektywy #pragma once. Ot powoduje ona,
e zawierajcy j plik bedzie wczany tylko raz podczas przegldania kodu przez
preprocesor. Kade sukcesywne wystpienie dyrektywy #include z tyme plikiem
zostanie zignorowane.
Zaawansowane C++ 340
Dyrektywa #pragma once jest obecnie obsugiwana przez bardzo wiele kompilatorw -
nie tylko przez Visual C++. Istnieje wic niemaa szansa, e niedugo podobna dyrektywa
stanie si czci standardu C++. Na pewno jednak nie bdzie to #pragma once, gdy
wszystkie szczegy dyrektyw #pragma s z zaoenia przynalene konkretnemu
kompilatorowi, a nie jzykowi C++ w ogle.
Jeli sam miabym optowa za jak konkretn, ustandaryzowan propozycj skadniow
dla tego rozwizania, to chyba najlepsze byoby po prostu #once.
***
I t sugesti dla Komitetu Standaryzacyjnego C++ zakoczylimy omawianie
preprocesora i jego dyrektyw :)
Podsumowanie
Ten rozdzia by powicony rzadko spotykanej w jzykach programowania waciwoci
C++, jak jest preprocesor. Moge z niego dowiedzie si wszystkiego na temat roli tego
wanego mechanizmu w procesie budowania programu oraz pozna jego dyrektywy.
Pozwoli ci to na sterowanie procesem kompilacji wasnego kodu.
W tym rozdziale staraem si te w jak najbardziej obiektywny sposb przedstawi makra
i makrodefinicje, gdy na ich temat wygasza si czsto wiele bdnych opinii. Chciaem
wic uwiadomi ci, e chocia wikszo dawnych zastosowa makr zostaa ju wyparta
przez inne konstrukcje jzyka, to makra s nadal przydatne w skracaniu zapisu czesto
wystpujcych fragmentw kodu oraz przede wszystkim - w kompilacji warunkowej.
Istnieje te wiele sposobw na wykorzystanie makr, ktre nosz znamiona trikw - by
moe natrafisz na takowe podczas lektury innych kursw, ksiek i dokumentacji. Warto
by wtedy pamita, e w stosowaniu makr, jak i we wszystkim w programowaniu, naley
zawsze umie znale rwnowag midzy efektownoci a efektywnoci kodowania.
Preprocesor oraz omwione wczeniej wskaniki byy naszym ostatnim spotkaniem z
krain starego C w obrbie krlestwa C++. Kolejne trzy rozdziay skupiaj si na
zaawansowanych cechach tego ostatniego: programowaniu obiektowym (ze szczeglnym
uwzgldnieniem przeciania operatorw), wyjtkach oraz szablonach. Wpierw
zobaczymy usprawnienia OOPu, jakie oferuje nam jzyk C++.
Pytania i zadania
Moesz uwaa, e preprocesor jest reliktem przeszoci, ale nie uchroni ci to od
wykonania obowizkowej pracy domowej! ;))
Pytania
1. Czym jest preprocesor? Kiedy wkracza do akcji i jak dziaa?
2. Na czym polega mechanizm rozwijania i zastpowania makr?
3. Jakie dwa rodzaje makr mona wyrni?
4. Na jakie problemy mona natrafi, jeeli sprbuje si zastosowa makra zamiast
bardziej odpowiednich, innych konstrukcji jzyka C++?
5. Jakie dwa zastosowania makr pozostaj nadal aktualne?
6. Jakie wyraenia moe zawiera warunek kompilacji dyrektyw #if i #elif?
7. Czym rni si dwa warianty dyrektywy #include?
8. Jak rol peni dyrektywa #pragma?
Preprocesor 341
wiczenia
1. Opracuj (klasyczne ju) makro wyznaczajce wiksz z dwch podanych wartoci.
2. (Trudniejsze) Odszukaj definicj klasy CIntArray z rozdziau o wskanikach i
przy pomocy preprocesora przerb j tak, aby mona by z niej korzysta dla
dowolnego typu danych.
3. Otwrz kod aplikacji rozwizujcej rwnania kwadratowe, ktr (mam nadziej)
napisae w rozdziale 1.4. Dodaj do niej kod pomocniczy, wywietlajcy warto
delta dla podanego rwnania; niech kompiluje si on tylko wtedy, gdy
zdefiniowana zostanie nazwa DEBUG.
4. (Trudne) Skonstruuj warunek kontrolowanej kompilacji, ktry pozwoli na
wykrycie platform 16-, 32- i 64-bitowych.
Wskazwka: wykorzystaj charakterystyk typu int
2
ZAAWANSOWANA
OBIEKTOWO
Nuda jest wrogiem programistw.
Bjarne Stroustrup
C++ jest zasuonym czonkiem licznej obecnie rodziny jzykw obiektowych. Oferuje on
wszystkie koniecznie mechanizmy, suce praktycznej realizacji idei programowania
zorientowanego obiektowo. Poznalimy je w dwch rozdziaach poprzedniej czci kursu.
Midzy C++ a innymi jzykami OOP wystpuj jednak pewne rnice. Nasz jzyk ma
wiele specyficznych dla siebie moliwoci, ktre maj za zadanie uatwienie ycia
programicie. Czsto te przyczyniaj si do powstania obiektywnie lepszych programw.
W tym rozdziale poznamy t wanie stron OOPu w C++. Przedstawione tu zagadnienia,
cho w zasadzie niezbdne do wystarczajcej znajomoci jzyka, s w duej czci
przydatnymi udogonieniami. Nie niezbdnymi, lecz wielce interesujcymi i praktycznymi.
Poznanie ich sprawi, e nasze obiektowe programy bd wygodne w konstruowaniu i
pniejszej modyfikacji. Programowanie stanie si po prostu atwiejsze i przyjemniejsze -
a to chyba bdzie bardzo znaczcym osigniciem.
Zobaczmy wic, jakie wyjtkowe konstrukcje OOP oferuje nam C++.
O przyjani
W czasie pierwszych spotka z programowaniem obiektowym wspominaem do czsto
o jego zaletach, wymieniajc wrd nich podzia kodu na drobne i atwe to zarzdzania
kawaki. Tymi fragmentami (take pod wzgldem koncepcyjnym) s oczywicie klasy.
Plusem, jaki niesie za soba stosowanie klas, jest wyodrbnienie kodu i danych w obiekty
zajmujce si konkretnymi zadaniami i reprezentujcymi konkretne obiekty. Instancje
klas wsppracuj ze sob i dziki temu wypeniaj zadania aplikacji. Tak to wyglda -
przynajmniej w teorii :)
Atutem klas jest niezaleno, zwana fachowo hermetyzacj lub enkapsulacj. Objawia
si ona tym, i dana klasa posiada pewien zestaw pl i metod, z ktrym tylko wybrane s
dostpne dla wiata zewntrznego. Jej wewntrzne sprawy s cakowicie chronione; su
ku temu specyfikatory dostpu, jak private i protected.
Opatrzone nimi skadowe s w zasadzie cakiem odseparowane od wiata zewntrznego,
bo ten jest dla nich potencjalnie grony. Upubliczniajc swoje pole klasa naraaaby
przecie swoje dane na przypadkowe lub celowe, ale zawsze niepodane modyfikacje.
To tak jakby wyj z domu i zostawi drzwi niezamknite na klucz: nie jest to wpradzie
bezporednie zaproszenie dla zodzieja, ale taka okazja moe go uczyni - w myl
znanego powiedzenia.
Ale przecie nie wszyscy s li - kady ma przynajmniej kilku przyjaci. Przyjaciel jest
to osoba, na ktr mona liczy; o ktrej wiemy, e nie zrobi nam nic zego. Wikszo
ludzi uwaa, e przyja jest w yciu bardzo wana - i nie musz nas do tego
Zaawansowane C++ 344
przekonywa adni socjologowie. Wszyscy wiemy to dobrze z wasnego, yciowego
dowiadczenia.
No dobrze, ale co to ma wsplnego z programowaniem? Ot bardzo wiele, zwaszcza z
programowaniem obiektowym. Mianowicie, klasa take moe mie przyjaci: mog
by nimi globalne funkcje, metody innych klas, a take inne klasy w caoci. C to
jednak znaczy, e klasa ma jakiego przyjaciela? Wyjanijmy wic, e:
Przyjaciel (ang. friend) danej klasy ma dostp do jej wszystkich skadnikw - take
tych chronionych, a nawet prywatnych.
Jeeli zatem klasa posiada przyjaciela, to oznacza to, e daa mu klucze (dostp) do
swojego mieszkania (niepublicznych skadowych). Przyjaciel klasy ma do nich prawie
takie samo prawo, jak metody teje klasy. Pewne drobne rnice wyjanimy sobie przy
okazji osobnego omwienia zaprzyjanionych funkcji i klas.
Dowiedzmy si teraz, jak zaprzyjani z klas jaki inny element programu. Jest
oczywicie i jak zwykle bardzo proste ;) Naley bowiem umieci w definicji klasy tzw.
deklaracj przyjani (ang. friend declaration):
friend deklaracja_przyjaciela;
Sowem kluczowym friend poprzedzamy w niej deklaracj_przyjaciela. T deklaracj
moe by:
prototyp funkcji globalnej
prototyp metody ze zdefiniowanej wczeniej klasy
nazwa zadeklarowanej wczeniej klasy
Oto najprostszy i niezbyt mdry przykad:
class CFoo
{
private:
std::string m_strBardzoOsobistyNapis;
public:
// konstruktor
CFoo() { m_strBardzoOsobistyNapis = "Kocham C++!"; }
// deklaracja przyjani z funkcj
friend void Wypisz(CFoo*);
};
// zaprzyjaniona funkcja
void Wypisz(CFoo* pFoo)
{
std::cout << pFoo->m_strBardzoOsobistyNapis;
}
Zaprzyjaniony byt - w tym przypadku funkcja - ma tu peen dostp do prywatnego pola
klasy CFoo. Moe wic wypisa jego zawarto dla kadego obiektu tej klasy, jaki
zostanie mu podany.
Deklaracja przyjani w tym przykadzie wydaje si by umieszczona w sekcji public
klasy CFoo. Tak jednak nie jest, gdy:
Zaawansowana obiektowo 345
Deklaracja przyjani moe by umieszczona w kadym miejscu definicji klasy i
zawsze ma to samo znaczenie.
Jest wic obojtne, gdzie si ona pojawi. Zwykle piszemy j albo na pocztku, albo na
kocu klasy, wyrniajc na przykad zmniejszonym wciciem. Pokazujemy w ten
sposb, e nie podlega ona specyfikatorom dostpu.
Nie ma wic czego takiego jak publiczna deklaracja przyjani lub prywatna deklaracja
przyjani. Przyjaciel pozostaje przyjacielem niezalenie od tego, czy si nim chwalimy,
czy nie.
Skoro teraz wiemy ju z grubsza, czym s przyjaciele klas, omwimy sobie osobno
zaprzyjanianie funkcji globalnych oraz innych klas i ich metod.
Funkcje zaprzyjanione
Najpierw zobaczymy, jak zaprzyjani klas z funkcj - tak, aby funkcja miaa dostp do
niepublicznych skadnikw z danej klasy.
Deklaracja przyjani z funkcj
Chcc uczyni jak funkcj przyjacielem klasy, musimy w definicji klasy poda
deklaracj zaprzyjanionej funkcji, poprzedzajc j sowem kluczowym friend.
Ilustracj tego faktu nie bdzie poniszy przykad. Mamy w nim klas opisujc okrg -
CCircle. Zaprzyjaniona z ni funkcja PrzecinajaSie() sprawdza, czy podane jej dwa
okrg maj punkty wsplne:
#include <cmath>
class CCircle
{
private:
// rodek okrgu
struct { float x, y; } m_ptSrodek;
// jego promie
float m_fPromien;
public:
// konstruktor
CCircle (float fPromien, float fX = 0.0f, float fY = 0.0f)
{ m_fPromien = fPromien;
m_ptSrodek.x = fX;
m_ptSrodek.y = fY; }
// deklaracja przyjani z funkcj
friend bool PrzecinajaSie(CCircle&, CCircle&);
};
// zaprzyjaniona funkcja
bool PrzecinajaSie(CCircle& Okrag1, CCircle& Okrag2)
{
// obliczamy odlego midzy rodkami
float fRoznicaX = Okrag2.m_ptSrodek.x - Okrag1.m_ptSrodek.x;
float fRoznicaY = Okrag2.m_ptSrodek.y - Okrag1.m_ptSrodek.y;
float fOdleglosc = sqrt(fRoznicaX*fRoznicaX + fRoznicaY*fRoznicaY);
Zaawansowane C++ 346
// odlego ta musi by mniejsza od sumy promieni, ale wiksza
// od ich bezwzgldnej rnicy
return (fOdleglosc < Okrag1.m_fPromien + Okrag2.m_fPromien
&& fOdleglosc > abs(Okrag1.m_fPromien - Okrag2.m_fPromien);
}
Bardzo dobrze wida tu ide przyjani: funkcja PrzecinajaSie() ma dostp do
skadowych m_ptSrodek oraz m_fPromien z obiektw klasy CCircle - mimo e s
prywatne pola klasy. CCircle deklaruje jednak przyja z funkcj PrzecinajaSie(), a
zatem udostpnia jej swoje osobiste dane.
Zauwamy jeszcze, e w deklaracji przyjani podajemy cay prototyp funkcji, a nie tylko
jej nazw. Moliwe jest bowiem zdefiniowanie kilku funkcji o tej nazwie, np. tak:
bool PrzecinajaSie(CCircle&, CCircle&);
bool PrzecinajaSie(CRectangle&, CRectangle&);
bool PrzecinajaSie(CPolygon&, CPolygon&);
// itd. (wraz z ewentualnymi kombinacjami krzyowymi)
Klasa bdzie jednak przyjania si tylko z t funkcj, ktrej deklaracj zamiecimy po
sowie friend. Zapamitajmy po prostu, e:
Jedna zwyka deklaracja przyjani oznacza przyja z jedn funkcj.
Na co jeszcze trzeba zwrci uwag
Wszystko wydawaoby si raczej proste. Nie zaszkodzi jednak powiedzie wprost o
pewnych oczywistych faktach zwizanych z zaprzyjanionymi funkcjami.
Funkcja zaprzyjaniona nie jest metod
Jedno swko friend moe bardzo wiele zmieni. Porwnajmy choby te dwie klasy:
class CFoo
{
public:
void Funkcja();
};
class CBar
{
public:
friend void Funkcja();
};
Rni si one tylko tym swkiem ale jest to rnica znaczca. W pierwszej klasie
Funkcja() jest jej metod: zadeklarowalimy j tak, jak wszystkie normalne metody
klas. Znamy to ju dobrze, gdy proces definiowania metod poznalimy przy pierwszym
spotkanie z OOPu. Do peni szczci na ley jeszcze tylko zdefiniowa ciao emtody
CFoo::Funkcja() i wszystko bdzie w porzdku.
Deklaracja w drugiej klasie jest natomiast opatrzona swkiem friend, ktre zupenie
zmienia jej znaczenie. Funkcja() nie jest tu metod klasy CBar. Jest wprawdzie
zaprzyjaniona z ni, ale nie jest jej skadnikiem: nie ma dostpu do wskanika this.
Aby z tej zaprzyjanionej funkcji mg by w ogle jaki uytek, trzeba jej zapewni
dostp do obiektu klasy CBar, bo jej samej nikt go nie da. Wobec braku parametrw
funkcji pewnie bdzie to wymagao zadeklarowania globalnej zmiennej obiektowej typu
CBar.
Zaawansowana obiektowo 347
Pamitaj zatem, i:
Funkcje zaprzyjanione z klas nie s jej skadnikami. Nie posiadaj dostpu do
wskanika this tej klasy, gdy nie s jej metodami.
W praktyce wic naley jako poda takiej funkcji obiekt klasy, ktra si z ni przyjani.
Zobaczylimy w poprzednim przykadzie, e prawie zawsze odbywa si to poprzez
parametry. Referencja do obiektu klasy CCircle bya parametrem zaprzyjanionej z ni
funkcji PrzecinajaSie(). Tylko posiadajc dostp do obiektu klasy, ktra si z ni
przyjani, funkcja zaprzyjaniona moe odnie jak korzy ze swojego
uprzywilejowanego statusu.
Deklaracja przyjani jest te deklaracj funkcji
Mamy te drugi wany fakt zwizany z deklaracj funkcji zaprzyjanionej.
Deklaracja przyjani jako prototyp funkcji
Ot, taka deklaracja przyjani jest jednoczenie deklaracj funkcji jako takiej. Musimy
zauway, e w zaprezentowanych przykadach funkcje, ktre byy przyjacielami klasy,
zostay zdefiniowane dopiero po definicji teje klasy. Wczeniej kompilator nic o nich nie
wiedzia - a mimo to pozwoli na ich zaprzyjanienie! Czy to jaka niedorbka?
Ale skd! Kompilator uznaje po prostu deklaracj przyjani z funkcj take za deklaracj
samej funkcji. Linijka ze sowem friend peni wic funkcj prototypu funkcji, ktra moe
by swobodnie zdefiniowana w zupenie innym miejscu. Z kolei wczeniejsze
prototypowanie funkcji, przed deklaracj przyjani, nie jest konieczne. Mwic po ludzku,
w poniszym kodzie:
bool PrzecinajaSie(CCircle&, CCircle&);
class CCircle
{
// (ciach - szczegy)
friend bool PrzecinajaSie(CCircle&, CCircle&);
};
// gdzie dalej definicja funkcji...
pocztkowy prototyp funkcji PrzecinajaSie(), umieszczony przed definicj CCircle, nie
jest koniecznie wymagany. Bez niego kompilator skorzysta po prostu z deklaracji
przyjani jak z normalnej deklaracji funkcji.
Deklaracja przyjani z funkcj moe by jednoczenie deklaracj samej funkcji.
Wczeniejsza wiedza kompilatora o istnieniu zaprzyjanianej funkcji nie jest niezbdna,
aby funkcja ta moga zosta zaprzyjaniona.
Dodajemy definicj
Najbardziej zaskakujce jest jednak to, e deklarujc przyja z jak funkcj moemy
t funkcj jednoczenie zdefiniowa! Nic nie stoi na przeszkodzie, aby po zakoczeniu
deklaracji nie stawia rednika, lecz otworzy nawias klamrowy i wpisa tre funkcji:
class CVector2D
{
private:
float m_fX, m_fY;
Zaawansowane C++ 348
public:
CVector2D(float fX = 0.0f, float fY = 0.0f)
{ m_fX = fX; m_fY = fY; }
// zaprzyjaniona funkcja dodajca dwa wektory
friend CVector2D Dodaj(CVector2D& v1, CVector2D& v2)
{ return CVector2D(v1.m_fX + v2.m_fX, v1.m_fY + v2.m_fY); }
};
Nie zapominajmy, e nawet wwczas funkcja zaprzyjaniona nie jest metod klasy -
pomimo tego, e jej umieszczenie wewntrz definicji klasy sprawia takie wraenie. W tym
przypadku funkcja Dodaj() jest nadal funkcj globaln - wywoujemy j bez pomocy
adnego obiektu, cho oczywicie przekazujemy jej obiekty CVector2D w parametrach i
taki te obiekt otrzymujemy z powrotem:
CVector2D vSuma = Dodaj(CVector2D(1.0f, 2.0f), CVector2D(0.0f, -1.0f));
Umieszczenie definicji funkcji zaprzyjanionej w bloku definicji klasy ma jednak pewien
skutek. Ot funkcja staje si wtedy funkcj inline, czyli jest rozwijana w miejscu swego
wywoania. Przypomina pod tym wzgldem metody klasy, ale jeszcze raz powtarzam, e
metod nie jest.
Moe najlepiej bdzie, jeli zapamitasz, e:
Wszystkie funkcje zdefiniowane wewntrz definicji klasy s automatycznie inline,
jednak tylko te bez swka friend s jej metodami. Pozostae s funkcjami
globalnymi, lecz zaprzyjanionymi z klas.
Klasy zaprzyjanione
Zaprzyjanianie klas z funkcjami globalnymi wydaje si moe nieco dziwnym
rozwizaniem (gdy czciowo amie zalet OOPu - hermetyzacj), ale niejednokrotnie
bywa przydatnym mechanizmem. Bardziej obiektowym podejciem jest przyja klas z
innymi klasami - jako caociami lub tylko z ich pojedynczymi metodami.
Przyja z pojedynczymi metodami
Wiemy ju, e moemy zadeklarowa przyja klasy z funkcj globaln. Teraz dowiemy
si, e przyjacielem moe by take inny rodzaj funkcji - metoda klasy.
Ponownie spojrzyj na odpowiedni przykad:
// deklaracja zapowiadajca klasy CCircle
class CCircle;
class CGeometryManager
{
public:
bool PrzecinajaSie(CCircle&, CCircle&);
};
class CCircle
{
// (pomijamy reszt)
friend bool CGeometryManager::PrzecinajaSie(CCircle&, CCircle&);
};
Zaawansowana obiektowo 349
Tym razem funkcja PrzecinajaSie() staa si skadow klasy CGeometryManager. To
bardziej obiektowe rozwizanie - tym bardziej dobre, e nie przeszkadza w
zadeklarowaniu przyjani z t funkcj. Teraz jednak klasa z CCircle przyjani si z
metod innej klasy - CGeometryManager. Odpowiedni zmian (do naturaln) wida
wic w deklaracji przyjani.
Przyja z metodami innych klas byaby bardzo podobna do przyjani z funkcjami
globalnymi gdyby nie jeden szkopu. Kompilator musi mianowicie zna deklaracj
zaprzyjanianej metody (CGeometryManager::PrzecinajaSie()) ju wczeniej. To
za wi si z koniecznoci zdefiniowania jej macierzystej klasy (CGeometryManager).
Do tego potrzebujemy jednak informacji o klasie CCircle, aby moga ona wystpi jako
typ agrumentu metody PrzecinajaSie(). Rozwizaniem jest deklaracja
zapowiadajca, w ktre informujemy kompilator, e CCircle jest klas, nie mwiac
jednak niczego wicej. Z takimi deklaracjami spotkalimy si ju wczeniej i jeszcze
spotkamy si nie raz - szczeglnie w kontekcie przyjani midzyklasowej.
Chwileczk! A co z t zaprzyjanian metod, CGeometryManager::PrzecinajaSie()?
Czyby miaa ona nie posiada dostpu do wskanika this, mimo e jest funkcj
skadow klasy?
Odpowied brzmi: i tak, i nie. Wszystko zaley bowiem od tego, o ktry wskanik this
nam dokadnie chodzi. Jeeli o ten pochodzcy od CGeometryManager, to wszystko jest w
jak najlepszym porzdku: metoda PrzecinajaSie() posiada go oczywicie, zatem ma
dostp do skadnikw swojej macierzystej klasy. Jeli natomiast mamy na myli klas
CCircle, to faktycznie metoda PrzecinajaSie() nie ma dojcia do wskanika this tej
klasy! Zgadza si to cakowicie z faktem, i funkcja zaprzyjaniona nie jest metod
klasy, ktra si z ni przyjani - tak wic nie posiada wskanika this tej klasy (tutaj
CCircle). Funkcja moe by jednak metod innej klasy (tutaj CGeometryManager), a
dostp do jej skadnikw bdzie mie zawsze - takie s przecie podstawowe zaoenia
programowania obiektowego.
Przyja z ca klas
Deklarujc przyja jednej klasy z metodami innej klasy, mona pj o krok dalej.
Dlaczego na przykad nie powiza przyjani od razu wszystkich metod pewnej klasy z
nasz? Oczywicie monaby pracowicie zadeklarowa przyja ze wszystkimi metodami
tamtej klasy, ale jest prostsze rozwizanie. Moe zaprzyjani jedn klas z drug.
Deklaracja przyjani z ca klas jest nad wyraz prosta:
friend class nazwa_zaprzyjanionej_klasy;
Zastpuje ona deklaracje przyjani ze wszystkimi metodami klasy o podanej nazwie,
wyszczeglnionymi osobno. Taka forma jest poza tym nie tylko krtsza, ale te ma kilka
innych zalet.
Wpierw jednak spjrzmy na przykad:
class CPoint;
class CRect
{
private:
// ...
public:
bool PunktWewnatrz(CPoint&);
};
Zaawansowane C++ 350
class CPoint
{
private:
float m_fX, m_fY;
public:
CPoint(float fX = 0.0f, float fY = 0.0f)
{ m_fX = fX; m_fY = fY; }
// deklaracja przyjani z Crect
friend class CRect;
};
Wyznanie przyjani, ktry czyni klasa CPoint, sprawia, e zaprzyjaniona klasa CRect ma
peen dostp do jej skadnikw niepublicznych. Metoda CRect::PunktWewnatrz() moe
wic odczyta wsprzdne podanego punktu i sprawdzi, czy ley on wewntrz
prostokta opisanego przez obiektt klasy CRect.
Zauwamy jednoczenie, e klasa CPoint nie ma tutaj podobnego dostpu do
prywatnych skadowych CRect. Klasa CRect nie zadeklarowaa bowiem przyjani z klas
CPoint. Wynika std bardzo wana zasada:
Przyja klas w C++ nie jest automatycznie wzajemna. Jeeli klasa A deklaruje
przyja z klas B, to klasa B nie jest od razu take przyjacielem klasy A. Obiekty klasy B
maj wic dostp do niepublicznych danych klasy A, lecz nie odwrotnie.
Do czsto aczkolwiek yczymy sobie, aby klasy wzajemnie deklaroway sobie przyja.
Jest to jak najbardziej moliwe: po prostu w obu klasach musz by deklaracje przyjani:
class CBar;
class CFoo
{
friend class CBar;
};
class CBar
{
friend class CFoo;
};
Wymaga to zawsze zastosowania deklaracji zapowiadajcej, gdy kompilator musi
wiedzie, e dana nazwa jest klas, zanim pozwoli na jej zastosowanie w konstrukcji
friend class. Nie musi natomiast zna caej definicji klasy, co byo wymagane dla
przyjani z pojedynczymi metodami. Gdyby tak byo, to wzajemna przyja klas nie
byaby moliwa. Kompilator zadowala si na szczcie sam informacj CBar jest klas,
bez wnikania w szczegy, i przyjmuje deklaracj przyjani z klas, o ktrej w zasadzie
nic nie wie.
Kompilator nie przyjmie natomiast deklaracji przyjani z pojedyncz metod nieznanej
bliej klasy. Sprawia to, e wybircza przyja dwch klas nie jest moliwa, bo
wymagaaby niemoliwego: zdefiniowania pierwszej klasy przed definicj drugiej oraz
zdefiniowania drugiej przed definicj pierwszej. To oczywicie niemoliwe, a kompilator
nie zadowoli si niestety sam deklaracj zapowiadajc - jak to czyni przy deklarowaniu
cakowitej przejani (friend class klasa;).
Zaawansowana obiektowo 351
Jeszcze kilka uwag
Przyja nie jest szczegolnie zawiym aspektem programowania obiektowego w C++.
Wypada jednak nieco ucili jej wpyw na pozostae elementy OOPu.
Cechy przyjani klas w C++
Przyja klas w C++ ma trzy znaczce cechy, na ktre chc teraz zwrci uwag.
Przyja nie jest automatycznie wzajemna
W prawdziwym yciu kto, kogo uwaamy za przyjaciela, ma zwykle to samo zdanie o
nas. To wicej ni naturalne.
W programowaniu jest inaczej. Mona to uzna za kolejny argument, i jest ono zupenie
oderwane od rzeczywistoci, a mona po prostu przyj to do wiadomoci. A prawda jest
taka:
Klasa deklarujca przyja udostpnia przyjacielowi swoje niepubliczne skadowe - lecz
nie powoduje to od razu, e klasa zaprzyjaniona jest tak samo otwarta.
Powiedziaem ju, e chcc stworzy wzajemny zwizek przyjani trzeba umieci
odpowiednie deklaracje w obu klasach. Wymaga to zawsze zapowiadajcej deklaracji
przynajmniej jeden z powizanych klas.
Przyja nie jest przechodnia
Inaczej mwic: przyjaciel mojego przyjaciela nie jest moim przyjacielem. Przekadajc
to na C++:
Jeeli klasa A deklaruje przyja z klas B, za klasa B z klas C, to nie znaczy to, e
klasa C jest od razu przyjacielem klasy A.
Gdybymy chcieli, eby tak byo, powinnimy wyranie to zadeklarowa:
friend class C;
Przyja nie jest dziedziczna
Przyja nie jest rwnie dziedziczona. Tak wic przyjaciel klasy bazowej nie jest
automatycznie przyjacielem klasy pochodnej. Aby tak byo, klasa pochodna musi sama
zadeklarowa swojego przyjaciela.
Mona to uzasadni na przykad w ten sposb, e deklaracja przyjani nie jest
skadnikiem klasy - tak jak metoda czy pole. Nie mona wic go odziedziczy. Inne
wytumaczenie: deklaracja friend nie ma przypisanego specyfikatora dostpu (public,
private), zatem nie wiadomo by byo, co z ni zrobi w procesie dziedziczenia; jak
wiemy, skadniki private nie s dziedziczone, a pozostae owszem
102
.
Dwie ostatnie uwagi moemy te uoglni do jednej:
Klasa ma tylko tych przyjaci, ktrych sama sobie zadeklaruje.
102
Jest tak, gdy stosujemy dziedziczenie publiczne (class pochodna : public bazowa), ale tak robimy niemal
zawsze.
Zaawansowane C++ 352
Zastosowania
Mwic o zastosowaniach przyjani, musimy rozgraniczy zaprzyjanione klasy i funkcje
globalne.
Wykorzystanie przyjani z funkcj
Do czego mog przyda si zaprzyjanione funkcje? Teoretycznie korzyci jest wiele,
ale w praktyce na przd wysuwa si jedno gwne zastosowanie. To przecianie
operatorw.
O tym uytecznym mechanimie jzyka bdziemy mwi w dalszej czci tego rozdziau.
Teraz mog powiedzie, e jest to sposb na zdefiniowanie wasnych dziaa
podejmowanych w stosunku do klas, ktrych obiekty wystpuj w wyraeniach z
operatorami: arytmetycznymi, bitowymi, logicznymi, i tak dalej. Precyzyjniej: chodzi o
stworzenie funkcji, ktre zostan wykonywane na argumentach operatorw, bdcych
naszymi klasami. Takie funkcje potrzebuj czsto dostpu do prywatnych skadnikw
klas, na rzecz ktrych przeciamy operatory. Tutaj wanie przydaj si funkcje
globalne, jako e zapewniaj taki dostp, a jednoczenie swobod definiowania kolejnoci
argumentw operatora.
Jeli nie bardzo to rozumiesz, nie przejmuj si. Przecianie operatorw jest w
rzeczywistoci bardzo proste, a zaprzyjanione funkcji globalne upraszczaj to jeszcze
bardziej. Wkrtce sam si o tym przekonasz.
Korzyci czerpane z przyjani klas
A co mona zyska zaprzyjaniajc klasy? Tutaj trudniej o konkretn odpowied.
Wszystko zaley od tego, jak zaprojektujemy swj obiektowy program. Warto jednak
wiedzie, e mamy tak wanie moliwo, jak zaprzyjanianie klas. Jak wszystkie z
pozoru nieprzydatne rozwizania, okae si ona uyteczna w najmniej spodziewanych
sytuacjach.
***
T pocieszajc konkluzj zakoczylimy omawianie przyjani klas i funkcji w C++.
Kolejnym elementem OOPu, na jakim skupimy swoj uwag, bd konstruktory. Ich rola
w naszym ulubionym jzyka jest bowiem wcale niebagatelna i nieogranicza si tylko do
inicjalizacji obiektw Zobaczmy sami.
Konstruktory w szczegach
Konstruktory peni w C++ wyjtkowo duo rl. Cho oczywicie najwaniejsza (i w
zasadzie jedyn powan) jest inicjalizacja obiektw - instancji klas, to niejako przy
okazji mog one dokonywa kilku innych, przydatnych operacji. Wszystkie one wi si
z tym gwnym zadaniem.
W tym podrozdziale nie bdziemy wic mwi o tym, co robi konstruktor (bo to wiemy),
ale jak moe to robi. Innymi sowy, dowiesz si, jak wykorzysta rne rodzaje
konstruktorw do wasnych szczytnych celw programistycznych.
Maa powtrka
Najpierw jednak przyda si mae powtrzenie wiedzy, ktra bdzie nam teraz przydatna.
Przy okazji moe j troch usystematyzujemy; powinno si te wyjani to, co do tej
pory mogo by dla ciebie ewentualnie niejasne.
Zaczniemy od przypomnienia konstruktorw, a pniej procesu inicjalizacji.
Zaawansowana obiektowo 353
Konstruktory
Konstruktor jest specjaln metod klasy, wywoywan podczas tworzenia obiektu. Nie
jest on, jak si czasem bdnie sdzi, odpowiedzialny za alokacj pamici dla obiektu,
lecz tylko za wstpne ustawienie jego pl. Niejako przy okazji moe on aczkolwiek
podejmowa te inne czynnoci, jak zwyka metoda klasy.
Cechy konstruktorw
Konstruktory tym jednak rni si od zwykych metod, i:
nie posiadaj wartoci zwracanej. Konstruktor nic nie zwraca (bo i komu?),
nawet typu pustego, czyli void. Zgoda, mona si spiera, e wynikiem jego
dziaania jest obiekt, lecz konstruktor nie jest jedynym mechanizmem, ktry
bierze udzia w jego tworzeniu: liczy si jeszcze alokacja pamici. Dlatego te
przyjmujemy, e konstruktor nie zwraca wartoci. Wida to zreszt w jego
deklaracji
nie mog by wywoywane za porednictwem wskanika na funkcje. Przyczyna
jest prosta: nie mona pobra adresu konstruktora
maj mnstwo ogranicze co do przydomkw w deklaracjach:
nie mona ich czyni metodami staymi (const)
nie mog by metodami wirtualnymi (virtual), jako e sposb ich
wywoywania w warunkach dziedziczenia jest zupenie odmienny od obu
typw metod: wirtualnych i niewirtualnych. Wspominaym o tym przy
okazji dziedziczenia.
nie mog by metodami statycznymi klas (static). Z drugiej strony
posiadaj unikaln cech metod statycznych, jak jest moliwo
wywoania bez koniecznoci posiadania obiektu macierzystej klasy.
Konstruktory maj jednak dostp do wskanika this na tworzony obiekt,
czego nie mona powiedzie o zwykych metodach statycznych
nie s dziedziczone z klas bazowych do pochodnych
Wida wic, e konstruktor to bardzo dziwna metoda: niby zwraca jak warto
(tworzony obiekt), ale nie deklarujemy mu wartoci zwracanej; nie moe by wirtualny,
ale w pewnym sensie jest; nie moe by statyczny, ale posiada cechy metod
statycznych; jest funkcj, ale nie mona pobra jego adresu, itd. To wszystko wydaje si
nieco zakrcone, lecz wiemy chyba, e nie przeszkadza to wcale w normalnym uywaniu
konstruktorw. Zamiast wic rozstrzsa fakty, czym te metody s, a czym nie, zajmijmy
si ich definiowaniem.
Definiowanie
W C++ konstruktor wyrnia si jeszcze tym, e jego nazwa odpowiada nazwie klasy, na
rzecz ktrej pracuje. Przykadowa deklaracja konstruktora moe wic wyglda tak:
class CFoo
{
private:
int m_nPole;
public:
CFoo(int nPole) { m_nPole = nPole; }
};
Jak widzimy, nie podajemy tu adnej wartoci zwracanej.
Przecianie
Zwyke metody klasy take mona przecia, ale w przypadku konstruktorw dzieje si
to nadzwyczaj czsto. Znowu posuymy si przykadem wektora:
Zaawansowane C++ 354
class CVector2D
{
private:
float m_fX, m_fY;
public:
// konstruktor, trzy sztuki
CVector2D() { m_fX = m_fY = 0.0f; }
CVector2D(float fDlugosc)
{ m_fX = m_fY = fDlugosc / sqrt(2); }
CVector2D(float fX, float fY) { m_fX = fX; m_fY = fY; }
};
Definiujc przecione konstruktory powinnimy, analogicznie jak w przypadku innych
metod oraz zwykych funkcji, wystrzega si niejednoznacznoci. W tym przypadku
powstaaby ona, gdyby ostatni wariant zapisa jako:
CVector2D(float fX = 0.0f, float fY = 0.0f);
Wwczas mgby on by wywoany z jednym argumentem, podobnie jak konstruktor
nr 2. Kompilator nie zdecyduje, ktry wariant jest lepszy i zgosi bd.
Konstruktor domylny
Konstruktor domylny (ang. default constructor), zwany te domniemanym, jest to
taki konstruktor, ktry moe by wywoany bez podawania parametrw.
W klasie powyej jest to wic pierwszy z konstruktorw. Gdybymy jednak ca trjk
zastpili jednym:
CVector2D(float fX = 0.0f, float fY = 0.0f) { m_fX = fX; m_fY = fY; }
to on take byby konstruktorem domylnym. Ilo podanych do niego parametrw moe
by bowiem rwna zeru. Wida wic, e konstruktor domylny nie musi by akurat tym,
ktry faktycznie nie posiada parametrw w swej deklaracji (tzw. parametrw
formalnych).
Naturalnie, klasa moe mie tylko jeden konstruktor domylny. W tym przypadku
oznacza to, e konstruktor w formie CVector2D(), CVector2D(float fDlugosc = 0.0f)
czy jakikolwiek inny tego typu nie jest dopuszczalny. Powstaaby bowiem
niejednoznaczno, a kompilator nie wiedziaby, ktr metod powinien wywoywa.
Za wygeneroowanie domylnego konstruktora moe te odpowiada sam kompilator.
Zrobi to jednak tylko wtedy, gdy sami nie podamy jakiegolwiek innego
konstruktora. Z drugiej strony, nasz wasny konstruktor domylny zawsze przesoni ten
pochodzcy od kompilatora. W sumie mamy wic trzy moliwe sytuacje:
nie podajemy adnego wasnego konstruktora - kompilator automatycznie
generuje domylny konstruktor publiczny
podajemy wasny konstruktor domylny (jeden i tylko jeden) - jest on uywany
podajemy wasne konstruktory, ale aden z nich nie moe by domylny, czyli
wywoywany bez parametrw - wwczas klasa nie ma konstruktora domylnego
Tak wic tylko w dwch pierwszych sytuacjach klasa posiada domylny konstruktor. Jaka
jest jednak korzy z jego obecnoci? Ot jest ona w sumie niewielka:
tylko obiekty posiadajce konstruktor domylny mog by elementami tablic.
Podkrelam: chodzi o obiekty, nie o wskaniki do nich - te mog by czone w
tablice bez wzgldu na konstruktory
Zaawansowana obiektowo 355
tylko klas posiadajc konstruktor domylny mona dziedziczy bez dodatkowych
zabiegw przy konstruktorze klasy pochodnej
T drug zasad wprowadziem przy okazji dziedziczenia, cho nie wspominaem o owych
dodatkowych zabiegach. Bd one treci tego podrozdziau.
Kiedy wywoywany jest konstruktor
Popatrzmy teraz na sytuacje, w ktrych pracuje knnstruktor. Nie jest ich zbyt wiele, tylko
kilka.
Niejawne wywoanie
Niejawne wywoanie (ang. implicit call) wystpuje wtedy, gdy to kompilator wywouje
nasz konstruktor. Jest par takich sytuacji:
najprostsza: gdy deklarujemy zmienn obiektow, np.:
CFoo Foo;
w momencie tworzenia obiektu, ktry zawiera w sobie pola bdce zmiennymi
obiektowymi innych klas
w chwili tworzenia obiektu klasy pochodnej jest wywoywany konstruktor klasy
bazowej
Jawne wywoanie
Konstruktor moemy te wywoa jawnie. Mamy wtedy wywoanie niejawne (ang. explicit
call), ktre wystpuje np. w takich sytuacjach:
przy konstruowaniu obiektu operatorem new
przy jawnym wywoaniu konstruktora: nazwa_klasy([parametry])
W tym drugim przypadku mamy tzw. obiekt chwilowy. Zwracalimy taki obiekt, kopiujc
go do rezultatu funkcji Dodaj(), prezentujc funkcje zaprzyjanione.
Inicjalizacja
Teraz powiemy sobie wicej o inicjalizacji. Jest to bowiem proces cile zwizany z
aspektami konstruktorw, ktre omwimy w tym podrozdziale.
Inicjalizacja (ang. initialization) jest to nadanie obiektowi wartoci pocztkowej w
chwili jego tworzenia.
Kiedy si odbywa
W naturalny sposb inicjalizacj wiemy z deklaracj zmiennych. Odbywa si ona
jednak take w innych sytuacjach.
Dwie kolejne zwizane z funkcjami. Ot jest to:
przekazanie wartoci poprzez parametr
zwrcenie wartoci jako rezultatu funkcji
Wreszcie, ostatnia sytuacja zwizana jest inicjalizacj obiektw klas - poznamy j za
chwil.
Jak wyglda
Inicjalizacja w oglnoci wyglda mniej wicej tak:
typ zmienna = inicjalizator;
Zaawansowane C++ 356
inicjalizator moe mie jednak rn posta, w zalenoci od typu deklarowanej
zmiennej.
Inicjalizacja typw podstawowych
W przypadku zmiennych typow elementarnych sprawa jest najprostsza. W inicjalizatorze
podajemy po prostu odpowiedni warto, jaka zostanie przypisana temu typowi, np.:
unsigned nZmienna = 42;
float fZmienna = 10.5;
Zauwamy, e bardzo czsto inicjalizacja zwizana jest niejawn konwersj wartoci do
odpowiedniego typu. Tutaj na przykad 42 (typu int) zostanie zamienione na typ
unsigned, za 10.5 (double) na typ float.
Agregaty
Bardziej zozone typy danych moemy inicjalizowa w specjalny sposb, jako tzw.
agregaty. Agregatem jest tablica innych agregatw (wzgldnie elementw typw
podstawowych) lub obiekt klasy, ktra:
nie dziedziczy z adnej klasy bazowej
posiada tylko skadniki publiczne (public, ewentualnie bez specyfikatora w
przypadku typw struct)
nie posiada funkcji wirtualnych
nie posiada zadeklarowanego konstruktora
Agregaty moemy inicjalizowa w specjalny sposb, podajc wartoci wszystkich ich
elementw (pl). Znamy to ju tablic, np.:
int aTablica[13] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41 };
Podobnie moe to si odbywa take dla struktur (tudzie klas), speniajcych cztery
podane warunki:
struct VECTOR3 { float x, y, z; };
VECTOR3 vWektor = { 6.0f, 12.5f, 0.0f };
W przypadku bardziej skomplikowanych, zagniedonych agregatw, bdziemy mieli
wicej odpowiednich par nawiasw klamrowych:
VECTOR3 aWektory[3] = { { 0.0f, 2.0f, -3.0f },
{ -1.0f, 0.0f, 0.0f },
{ 8.0f, 6.0f, 4.0f } };
Mona je aczkolwiek opuci i napisa te 9 wartoci jednym cigiem, ale przyznasz
chyba, e w tej postaci inicjalizacja wyglda bardziej przejrzycie. Po inicjalizatorze wida
przynajmniej, e inicjujemy tablic trj-, a nie dziewicioelementow.
Inicjalizacja konstruktorem
Ostatni sposb to inicjalizacja obiektu jego wasnym konstruktorem - na przykad:
std::string strZmienna = "Hmm...";
Tak, to jest jak najbardziej taki wanie przykad. W rzeczywistoci kompilator rozwinie
go bowiem do:
std::string strZmienna("Hmm...");
Zaawansowana obiektowo 357
gdy w klasie std::string istnieje odpowiedni konstruktor przyjmujcy jeden argument
typu napisowego
103
:
string(const char[]);
Konstruktor jest tu wic wywoywany niejawnie - jest to tak zwany konstruktor
konwertujcy, ktremu przyjrzymy si bliej w tym rozdziale.
Listy inicjalizacyjne
W definicji konstruktora moemy wprowadzi dodatkowy element - tzw. list
inicjalizacyjn:
nazwa_klasy::nazwa_klasy([parametry]) : lista_inicjalizacyjna
{
ciao_konstruktora
}
Lista inicjalizacyjna (ang. initializers list) ustala sposb inicjalizacji obiektw tworzonej
klasy.
Za pomoc takiej listy moemy zainicjalizowa pola klasy (i nie tylko) jeszcze przed
wywoaniem samego konstruktora. Ma to pewne konsekwencje i bywa przydatne w
okrelonych sytuacjach.
Inicjalizacja skadowych
Dotychczas dokonywalimy inicjalizacji pl klasy w taki oto sposb:
class CVector2D
{
private:
float m_fX, m_fY;
public:
CVector2D(float fX = 0.0f, float fY = 0.0f)
{ m_fX = fX; m_fY = fY; }
};
Przy pomocy listy inicjalizacyjnej zrobimy to samo mniej wicej tak:
CVector2D(float fX = 0.0f, float fY = 0.0f) : m_fX(fX), m_fY(fY) { }
Jaka jest rnica?
konstruktor moe u nas by pusty. To najprawdopodobniej sprawi, e kompilator
zastosuje wobec niego jak optymalizacj
dziaania m_fX(fX) i m_fY(fY) (zwrmy uwag na skadni), maj charakter
inicjalizacji pl, podczas gdy przypisania w ciele konstruktora s przypisaniami
wanie
lista inicjalizacyjna jest wykonywana jeszcze przed wejciem w ciao
konstruktora i wykonaniem zawartych tam instrukcji
Drugi i trzeci fakt jest bardzo wany, poniewa daj nam one moliwo umieszczania w
klasie takich pl, ktre nie moga oby si bez inicjalizacji, a wic:
103
W rzeczywistoci ten konstruktor wyglda znacznie obszerniej, bo w gr wchodz jeszcze szablony z
biblioteki STL. Nic jednak nie staoby na przeszkodzie, aby tak to wanie wygldao.
Zaawansowane C++ 358
staych (pl z przydomkiem const)
staych wskanikw (typ* const)
referencji
obiektw, ktrych klasy nie maj domylnych konstruktorw
Lista inicjalizacyjna gwarantuje, e zostan one zainicjalizowane we waciwym czasie -
podczas tworzenia obiektu:
class CFoo
{
private:
const float m_fPole;
// nie moe by: const float m_fPole = 42; !!
public:
// konstruktor - inicjalizacja pola
CFoo() : m_fPole(42)
{
/* m_fPole = 42; // te nie moe by - za pno!
// m_fPole musi mie warto ju
// na samym pocztku wykonywania
// konstruktora */
}
};
Mwiem te, e inicjalizacja przy pomocy listy inicjalizacyjnej jest szybsza od przypisa
w ciele konstruktora. Powinnimy wic stosowa j, jeeli mamy tak moliwo, a
decyzja na ktrej z dwch rozwiza nie robi nam rnicy. Zauwamy te, e zapis na
licie inicjalizacyjnej jest po prostu krtszy.
W licie inicjalizacyjnej moemy umieszcza nie tylko czyste stae i argumenty
konstruktora, lecz take zloone wyraenia - nawet z wywoaniami metod czy funkcji
globalnych. Nie ma wic adnych ogranicze w stosunku do przypisania.
Wywoanie konstruktora klasy bazowej
Lista inicjalizacyjna pozwala zrobi co jeszcze zanim waciwy konstruktor ruszy do
pracy. Pozwala to nie tylko na inicjalizacj skadowych klasy, ktre tego wymagaj, ale
take - a moe przede wszystkim - wywoanie konstruktorw klas bazowych.
Przy pierwszym spotkaniu z dziedziczeniem mwiem, e klasa, ktra ma by
dziedziczona, powinna posiada bezparametrowy konstruktor. Byo to spowodowane
kolejnoci wywoywania konstruktorw: jak wiemy, najpierw pracuje ten z klasy
bazowej (poczynajc od najstarszego pokolenia), a dopiero potem ten z klasy pochodnej.
Kompilator musi wic wiedzie, jak wywoa konstruktor z klasy bazowej. Jeeli nie
pomoemy mu w decyzji, to uprze si na konstruktor domyslny - bezparametrowy.
Teraz bdziemy ju wiedzie, jak mona pomc kompilatorowi. Suy do tego wanie
lista inicjalizacyjna. Oprcz inicjalizacji pl klasy moemy te wywoywa w niej
konstruktory klas bazowych. W ten sposb zniknie konieczno posiadania przez nie
konstruktora domylnego.
Oto jak moe to wyglda:
class CIndirectBase
{
protected:
int m_nPole1;
Zaawansowana obiektowo 359
public:
CIndirectBase(int nPole1) : m_nPole1(nPole) { }
};
class CDirectBase : public CIndirectBase
{
public:
// wywoanie konstruktora klasy bazowej
CDirectBase(int nPole1) : CIndirectBase(nPole1) { }
};
class CDerived : public CDirectBase
{
protected:
float m_fPole2;
public:
// wywoanie konstruktora klasy bezporednio bazowej
CDerived(int nPole1, float fPole2)
: CDirectBase(nPole1), m_fPole2(fPole2) { }
};
Zwrmy uwag szczeglnie na klas CDerived. Jej konstruktor wywouje konstruktor z
klasy bazowej bezporedniej - CDirectBase, lecz nie z poredniej - CIndirectBase. Nie
ma po prostu takiej potrzeby, gdy za relacje midzy konstruktorami klas CDirectBase i
CIndirectBase odpowiada tylko ta ostatnia. Jak zreszt wida, wywouje ona jedyny
konstruktor CIndirectBase.
Spjrzmy jeszcze na parametry wszystkich konstruktorw. Jak wida, zachowuj one
parametry konstruktorw klas bazowych - zapewne dlatego, e same nie potrafi poda
dla nich sensownych danych i bd ich da od twrcy obiektu. Uzyskane dane
przekazuj jednak do swoich przodkw; powstaje w ten sposb swoista sztafeta, w ktrej
dane z konstruktora najniszego poziomu dziedziczenia trafiaj w kocu do klasy
bazowej. Po drodze s one przekazywane z rk do rk i ewentualnie zostawiane w polach
klas porednich.
Wszystko to dzieje si za porednictwem list inicjalizacyjnej. W praktyce ich
wykorzystanie eliminuje wic bardzo wiele sytuacji, ktre wymagaj definiowania ciaa
konstruktora. Sam si zreszt przekonasz, e cae mnstwo pisanych przez ciebie klas
bedzie zawierao puste konstruktory, realizujce swoje funkcje wycznie poprzez listy
inicjalizacyjne.
Konstruktory kopiujce
Teraz porozmawiamy sobie o kopiowaniu obiektw, czyli tworzeniu ich koncepcyjnych
duplikatw. W C++ mamy na to dwie wydzielone rodzaje metod klas:
konstruktory kopiujce, tworzce nowe obiekty na podstawie ju istniejcych
przecione operatory przypisania, ktrych zadaniem jest skopiowanie stanu
jednego obiektu do drugiego, ju istniejcego
Przecianiem operatorw zajmiemy si dalszej czci rozdziau. W tej sekcji przyjrzymy
si natomiast konstruktorom kopiujcym.
O kopiowaniu obiektw
Wydawaoby si, e nie ma nic prostszego od skopiowania obiektu. Okazuje si jednak,
e czsto nieodzowne s specjalne mechanizmy temu suce Sprawdmy to.
Zaawansowane C++ 360
Pole po polu
Gdy mwimy o kopiowaniu obiektw i nie zastanawiamy si nad tym duej, to sdzimy,
e to po prostu skopiowanie danych - zawartoci pl - z jednego obszaru pamici do
drugiego. Przykadowo, spjrzmy na dwa wektory:
CVector2D vWektor1(1.0f, 2.0f, 3.0f);
CVector2D vWektor2 = vWektor1;
Cakiem susznie oczekujemy, e po wykonaniu kopiowania vWektor1 do vWektor2 oba
obiekty bd miay identyczne wartoci pl. W przypadku takich struktur danych jak
wektory, jest to zupenie wystarczajce. Dlaczego? Ot wszystkie ich pola s cakowicie
odrbnymi zmiennymi - nie maj adnych koneksji z otaczajcym je wiatem. Trudno
przecie oczekiwa, eby liczby typu float robiy cokolwiek innego poza
przechowywaniem wartoci. Ich proste skopiowanie jest wic waciwym sposobem
wykonania kopii wektora - czyli obiektu klasy CVector2D.
Samowystarczalne obiekty mog by kopiowane poprzez dosowne przepisanie wartoci
swoich pl.
Gdy to nie wystarcza
Nie wszyscy obiekty podpadaj jednak pod ustanowion wyej kategori. Czy pamitasz
moe klas CIntArray, ktr pokazaem, omawiajc wskaniki? Jeli nie, to spjrz
jeszcze raz na jej definicj (usprawnion wykorzystaniem list inicjalizacyjnych):
class CIntArray
{
// domylny rozmiar tablicy
static const unsigned DOMYSLNY_ROZMIAR = 5;
private:
// wskanik na waciw tablic oraz jej rozmiar
unsigned m_uRozmiar;
int* m_pnTablica;
public:
// konstruktory
CIntArray() // domylny
: m_uRozmiar(DOMYSLNY_ROZMIAR),
m_pnTablica(new int [m_uRozmiar]) { }
CIntArray(unsigned uRozmiar) // z podaniem rozmiaru tablicy
: m_uRozmiar(uRozmiar);
m_pnTablica(new int [m_uRozmiar]) { }
// destruktor
~CIntArray() { delete[] m_pnTablica; }
//-------------------------------------------------------------
// pobieranie i ustawianie elementw tablicy
int Pobierz(unsigned uIndeks) const
{ if (uIndeks < m_uRozmiar) return m_pnTablica[uIndeks];
else return 0; }
bool Ustaw(unsigned uIndeks, int nWartosc)
{ if (uIndeks >= m_uRozmiar) return false;
m_pnTablica[uIndeks] = uWartosc;
return true; }
// inne
Zaawansowana obiektowo 361
unsigned Rozmiar() const { return m_uRozmiar; }
};
Pytanie brzmi: jak skopiowa tablic typu CIntArray? Niby nic prostszego:
CIntArray aTablica1;
CIntArray aTablica2 = aTablica1; // hmm...
W rzeczywistoci mamy tu bardzo powany bd. Metoda pole po polu zupenie nie
sprawdza si w przypadku tej klasy. Problemem jest pole m_pnTablica: jesli skopiujemy
ten wskanik, to otrzymamy nic innego, jak tylko kopi wskanika. Bdzie si on
odnosi do tego samego obszaru pamici. Zamiast wic dwch fizycznych tablic mamy
tylko jedn, a obiekty Tablica1 i Tablica2 to jedynie kopie opakowa dla wskanika na t
tablic. Odwoujc si do danych, zapisanych w rzekomo odrbnych tablicach klasy
CIntArray, faktycznie bdziemy odnosi si do tych samych elementw! To powany
bd, co gorsza niewykrywalny a do momentu wyprodukowania bdnych rezultatw
przez program.
Co wic trzeba z tym zrobi - domylasz si, e rozwizaniem s tytuowe konstruktory
kopiujce. Jeszcze zanim je poznamy, powiniene zapamita:
Jeeli obiekt pracuje na jakim zewntrznym zasobie (np. pamici operacyjnej) i posiada
do niego odwoanie (np. wskanik), to jego klas koniecznie naley wyposay w
konstruktor kopiujcy. Bez niego zostanie bowiem podczas kopiowanie obiektu zostanie
skopiowane samo odwoanie do zasobu (czyli wskanik) zamiast stworzenia jego
duplikatu (czyli alokacji nowej porcji pamici).
Trzeba te wiedzie, e konieczno zdefiniowania konstruktora kopiujcego zwykle
automatycznie pociga za sob wymg obecnoci przecionego operatora przypisania.
Konstruktor kopiujcy
Zobaczmy zatem, jak dziaaj te cudowne konstruktory kopiujce. Jednak oprcz
zachwycania si nimi poznamy take sposb ich uycia (definiowania) w C++.
Do czego suy konstruktor kopiujcy
Konstruktor kopiujcy (ang. copy constructor) suy do tworzenia nowego obiektu
danej klasy na podstawie ju istniejcego, innego obiektu tej klasy.
Konstruktor ten, jak wszystkie konstruktory, wkracza do akcji podczas kreowania nowego
obiektu klasy. Czym si w takim razie rni od zwykego konstruktora? Przypomnijmy
dwie sporne linijki z poprzedniego paragrafu:
CIntArray aTablica1;
CIntArray aTablica2 = aTablica1;
Pierwsza z nich to normalne stworzenie obiektu klasy CIntArray. Pracuje tu zwyky
konstruktor, domylny zreszt.
Natomiast druga linijka moe by take zapisana jako:
CIntArray aTablica2 = CIntArray(aTablica1);
albo nawet:
CIntArray aTablica2(aTablica1);
Zaawansowane C++ 362
W niej pracuje konstruktor kopiujcy, gdy dokonujemy tu inicjalizacji nowego
obiektu starym.
Konstruktor kopiujcy jest wywoywany w momencie inicjalizacji nowotworzonego
obiektu przy pomocy innego obiektu tej samej klasy. Z tego powodu taki konstruktor
jest rwnie zwany inicjalizatorem kopiujcym.
Zaraz, jak to - przecie nie zdefiniowalimy dotd adnego specjalnego konstruktora! Jak
wic mg on by uyty w kodzie powyej?
Owszem, to prawda, ale kompilator wykona robot za nas. Jeli nie zdefiniujemy
wasnego konstruktora kopiujcego, to klasa zostanie obdarzona jego najprostszym
wariantem. Bdzie on wykonywa zwyke kopiowanie wartoci - dla nas cakowicie
niewystarczajce.
Musimy zatem wiedzie, jak definiowa wasne konstruktory kopiujce.
Konstruktor kopiujcy a przypisanie - rnica maa lecz wana
Moesz spyta: a co kompilator zrobi w takiej sytuacji:
CIntArray aTablica1;
CIntArray aTablica2;
aTablica1 = aTablica2; // a co to jest?...
Czy w trzeciej linijce take zostanie wywoany konstruktor kopiujcy?
Ot nie. Nie jest bowiem inicjalizacja (a wtedy przecie pracuje konstruktor kopiujcy),
lecz zwyke przypisanie. Nie tworzymy tu nowego obiektu, lecz przypisujemy jeden ju
istniejcy obiekt do drugiego istniejcego obiektu. Wobec braku aktu kreacji nie ma tu
miejsca dla adnego konstruktora.
Zamiast tego kompilator posuguje si operatorem przypisania. Jeeli go przeciymy (a
nauczymy si to robi ju w tym rozdziale), zdefiniujemy wasn akcj dla przypisywania
obiektw. W przypadku klasy CIntArray jest to niezbdne, bo nawet obecno
konstruktora kopiujcego nie spowoduje, e zaprezentowany wyej kod bdzie
poprawny. Konstruktorw nie dotyczy przecie przypisanie.
Dlaczego konstruktor kopiujcy
Ale w takim razie po co nam konstruktor kopiujcy? Przecie jego praca jest w wikszoci
normalnych sytuacji rwnowana:
wywoaniu zwykego konstruktora (czyli normalnemu stworzeniu obiektu)
wywoaniu operatora przypisania
Czy tak?
C, niezupenie. W zasadzie zgadza si to tylko dla takich obiektw, dla ktrych
wystarczajce jest kopiowanie pole po polu. Dla nich faktycznie nie potrzeba
specjalnego konstruktora kopiujcego. Jeli jednak mamy do czynienia z tak klas, jak
CIntArray, konstruktor taki jest konieczny. Sposb jego pracy bdzie si rni od
zwykego przypisania - wemy choby pod uwag to, e konstruktor pracuje na pustym
obiekcie, natomiast przypisanie oznacza zastpienie jednego obiektu drugim
Dokadniej wyjanimy t spraw, gdy poznamy przecianie operatorw. Teraz
zobaczmy, jak moemy zdefiniowa wasny konstruktor kopiujcy.
Definiowanie konstruktora kopiujcego
Skadni definicji konstruktora kopiujcego moemy zapisa tak:
nazwa_klasy::nazwa_klasy([const] nazwa_klasy& obiekt)
Zaawansowana obiektowo 363
{
ciao_konstruktora
}
Bierze on jeden parametr, bdcy referencj do obiektu swojej macierzystej klasy.
Obiekt ten jest podstaw kopiowania - inaczej mwic, jest to ten obiekt, ktrego kopi
ma zrobi konstruktor. W inicjalizacji:
CIntArray aTablica2 = aTablica1;
parametrem konstruktora kopiujcego bdzie wic aTablica1, za tworzonym obiektem-
kopi Tablica2. Wida to nawet lepiej w rwnowanej linijce:
CIntArray aTablica2(aTablica1);
Pozostaje jeszcze kwestia swka const w deklaracji parametru konstruktora. Cho
teoretycznie jest ona opcjonalna, to w praktyce trudno znale powd na uzasadnienie jej
nieobecnoci. Bez niej konstruktor kopiujcy mgby bowiem potencjalnie
zmodyfikowa kopiowany obiekt! Innym skutkiem byaby te niemono
kopiowania obiektw chwilowych.
Zapamitaj wic:
Parametr konstruktora kopiujcego praktycznie zawsze musi by sta referencj.
Inicjalizator klasy CIntArray
Gdy wiemy ju, do czego su konstruktory kopiujce i jak si je definiuje, moemy t
wiedz wykorzysta. Zdefiniujmy inicjalizator dla klasy, ktra tak bardzo go potrzebuje -
CIntArray.
Nie bdzie to trudne, jeeli zastanowimy si wpierw, co ten konstruktor ma robi. Ot
powinien on zaalokowa pamie rwn rozmiarowi kopiowanej tablicy oraz przekopiowa
z niej dane do nowego obiektu. Proste? Zatem do dziea:
#include <memory.h>
CIntArray::CIntArray(const CIntArray& aTablica)
{
// alokujemy pami
m_uRozmiar = aTablica.m_uRozmiar;
m_pnTablica = new int [m_uRozmiar];
// kopiujemy pami ze starej tablicy do nowej
memcpy (m_pnTablica, aTablica.m_pnTablica, m_uRozmiar * sizeof(int));
}
Po dodaniu tego prostego kodu tworzenie tablicy na podstawie innej, ju istniejcej:
CIntArray aTablica2 = aTablica1;
jest ju cakowicie poprawne.
Konwersje
Trzecim i ostatnim aspektem konstruktorw, jakim si tu zajmiemy, bedzie ich
wykorzystanie do konwersji typw. Temat ten jest jednak nieco szerszy ni
wykorzystanie samych tylko konstruktorw, wic omwimy go sobie w caoci.
Zaawansowane C++ 364
Konwersje niejawne (ang. implicit conversions) mog nam uatwi programowanie - jak
wikszo rzeczy w C++ :) W tym przypadku pozwalaj na przykad uchroni si od
koniecznoci definiowania wielu przecionych funkcji.
Najlepsz ilustracj bdzie tu odpowiedni przykad. Akurat tak si dziwnie skada, e
podrczniki programowania podaj tu najczciej jak klas zoonych liczb. Nie warto
naruszac tej dobrej tradycji - zatem spjrzmy na tak oto klas liczby wymiernej:
class CRational
{
private:
// licznik i mianownik
int m_nLicznik;
int m_nMianownik;
public:
// konstruktor
CRational(int nLicznik, int nMianownik)
: m_nLicznik(nLicznik), m_nMianownik(nMianownik) { }
//-------------------------------------------------------------
// metody dostpowe
int Licznik() const { return m_nLicznik; }
void Licznik(int nLicznik) { m_nLicznik = nLicznik; }
int Mianownik() const { return m_nMianownik; }
void Mianownik(int nMianownik)
{ m_nMianownik = (nMianownik != 0 ? nMianownik : 1); }
};
Napiszemy teraz funkcj mnoc przez siebie dwie takie liczby (czyli dwa uamki). Jeli
nie spalimy na lekcjach matematyki w szkole podstawowej, to bdzie ona wygldaa
chociaby tak:
CRational Pomnoz(const CRational& Liczba1, const CRational& Liczba2)
{
return CRational(Liczba1.Licznik() * Liczba2.Licznik(),
Liczba1.Mianownik() * Liczba2.Mianownik());
}
Moemy teraz uywa naszej funkcji na przykad w ten sposb:
CRational Raz(1, 2), Dwa(2, 3);
CRational Wynik = Pomnoz(Raz, Dwa);
Niestety, jest pewna niedogodno. Nie moemy zastosowa np. takiego wywoania:
CRational Wynik = Pomnoz(Raz, 5);
Drugi argument nie moe by bowiem typu int, lecz musi by obiektem typu CRational.
To niezbyt dobrze: wiemy przecie, e 5 (i kada liczba cakowita) jest take liczb
wymiern.
My to wiemy, ale kompilator nie. W tej sekcji poznamy zatem sposoby na informowanie
go o takich faktach - czyli wanie niejawne konwersje.
Sposoby dokonywania konwersji
Sprecyzujmy, o co nam waciwie chodzi. Ot chcemy, aby liczby cakowite (typu int)
mogy by przez kompilator interpretowane jako obiekty naszej klasy CRational.
Zaawansowana obiektowo 365
Fachowo mwimy, e chcemy zdefiniowa sposb konwersji typu int na typ CRational.
Wanie o takich konwersjach bdziemy mwi w niniejszym paragrafie. Poznamy dwa
sposoby na realizacj automatycznej zamiany typw w C++.
Konstruktory konwertujce
Pierwszym z nich jest tytuowy konstruktor konwertujcy.
Konstruktor z jednym obowizkowym parametrem
Konstruktor konwertujcy moe przyjmowa dokadnie jeden parametr
okrelonego typu i wykonywa jego konwersj na typ swojej klasy.
Jest to ten mechanizm, ktrego aktualnie potrzebujemy. Zdefiniujmy wic konstruktor
konwertujcy w klasie CRational:
CRational::CRational(int nLiczba)
: m_nLicznik(nLiczba), m_nMianownik(1) { }
Od tej pory wywoanie typu:
CRational Wynik = Pomnoz(Raz, 5);
albo nawet:
CRational Wynik = Pomnoz(14, 5);
jest cakowicie poprawne. Kompilator wie bowiem, w jaki sposb zamieni obiekt typu
int na obiekt typu CRational.
To samo osigna mona nawet prociej. Zasada jeden argument dla konstruktora
konwertujcego dziaa tak samo jak brak argumentw dla konstruktora domylnego. A
zatem dodatkowe argumenty mog by, lecz musz mie wartoci domylne.
W naszej klasie moemy wic po prostu zmodyfikowa normalny konstruktor:
CRational(int nLicznik, int nMianownik = 1)
: m_nLicznik(nLicznik), m_nMianownik(nMianownik) { }
W ten sposb za jednym zamachem mamy normalny konstruktor, jak te konwertujcy.
Ba, mona pj nawet jeszcze dalej:
CRational(int nLicznik = 0, int nMianownik = 1)
: m_nLicznik(nLicznik), m_nMianownik(nMianownik) { }
Ten konstruktor moe by wywoany bez parametrw, z jednym lub dwoma. Jest on wic
jednoczenie domylny i konwertujcy. Duy efekt maym kosztem.
Konstruktor konwertujcy nie musi koniecznie definiowa konwersji z typu
podstawowego. Moe wykorzystywa dowolny typ. Popatrzmy na to:
class CComplex
{
private:
// cz rzeczywista i urojona
float m_fRe;
float m_fIm;
public:
Zaawansowane C++ 366
// zwyky konstruktor (ktry jest rwnie domylny
// oraz konwertujcy z float do CComplex)
CComplex(float fRe = 0, float fIm = 0)
: m_fRe(fRe), m_fIm(fIm) { }
// konstruktor konwertujcy z CRational do CComplex
CComplex(const CRational& Wymierna)
: m_fRe(Wymierna.Licznik()
/ (float) Wymierna.Mianownik()),
m_fIm(0) { }
//-------------------------------------------------------------
// metody dostpowe
float Re() const { return m_fRe; }
void Re(float fRe) { m_fRe = fRe; }
float Im() const { return m_fIm; }
void Im(float fIm) { m_fIm = fIm; }
};
Klasa CComplex posiada zdefiniowane konstruktory konwertujce zarwno z float, jak i
CRational. Poza tym, e odpowiada to oczywistemu faktowi, i liczby rzeczywiste i
wymierne s take zespolone, pozwala to na napisanie takiej funkcji:
CComplex Dodaj(const CComplex& Liczba1, const CComplex& Liczba2)
{
return CComplex(Liczba1.Re() + Liczba2.Re(),
Liczba2.Im() + Liczba2.Im());
}
oraz wywoywanie jej zarwno z parametrami typu CComplex, jaki CRational i float:
CComplex Wynik;
Wynik = Dodaj(CComplex(1, 5), 4);
Wynik = Dodaj(CRational(10, 3), CRational(1, 3));
Wynik = Dodaj(1, 2);
// itd.
Mona zapyta: Czy konstruktor konwertujcy z float do CComplex jest konieczny?
Przecie jest ju jeden, z float do CRational, i drugi - z CRational do CComplex. Oba
robi w sumie to, co trzeba! Tak, to byaby prawda. W sumie jednak jest to bardzo
gboko ukryte. Istot niejawnych konwersji jest wanie to, e s niejawne: programista
nie musi si o nie martwi. Z drugiej strony oznacza to, e pewien kod jest wykonywany
za plecami kodera. Przy jednej niedosownej zamianie nie jest to raczej problemem, ale
przy wikszej ich liczbie trudno byoby zorientowa si, co tak naprawd jest zamieniane
w co.
Oprcz tego jest jeszcze bardziej prozaiczny powd: gdyby pozwala na wielokrotne
konwersje, kompilator musiaby sprawdza mnstwo potencjalnych drg konwersji.
Znacznie wyduyoby to czas kompilacji.
Nie jest wic dziwne, e:
Kompilator C++ dokonuje zawsze co najwyej jednej niejawnej konwersji
zdefiniowanej przez programist.
Nie jest przy tym wane, czy do konwersji stosujemy konstruktory czy te operatory
konwersji, ktre poznamy w nastpnym akapicie.
Zaawansowana obiektowo 367
Swko explicit
Dowiedzielimy si, e kady jednoargumentowy konstruktor definiuje konwersj
typu swojego parametru do typu klasy konstruktora. W ten sposb moemy okrela, jak
kompilator ma zamieni jaki typ (na przykad wbudowany lub inn klas) w typ naszych
obiektw.
atwo przeoczy fakt, e t drog jednoargumentowy konstruktor (ktry jest w sumie
konstruktorem jak kady inny) nabiera nowego znaczenia. Ju nie tylko inicjalizuje
obiekt swej klasy, ale i podaje sposb konwersji.
Dotd mwilimy, e to dobrze. Nie zawsze jednak tak jest. Czasem piszemy w klasie
jednoparametrowy konstruktor wcale nie po to, aby ustali jakkolwiek konwersj.
Nierzadko bowiem tego wymaga logika naszej klasy. Spjrzmy chociaby na konstruktor
z CIntArray:
CIntArray(unsigned uRozmiar)
: m_uRozmiar(uRozmiar);
m_pnTablica(new int [m_uRozmiar]) { }
Przyjmuje on parametr typu int - rozmiar tablicy. Niestety (tak, niestety!) jest tutaj
take konstruktorem konwertujcym z typu int na typ CIntArray. Z tego powodu
zupenie poprawne staje si bezsensowne przypisanie
104
w rodzaju:
CIntArray aTablica;
aTablica = 10; // Oj! Tworzymy 10-elementow tablic!
W powyszym kodzie tworzona jest tablica o odpowiedniej liczbie elementw i
przypisywana zmiennej Tablica. Na pewno nie moemy na to pozwoli - takie
przypisanie to przecie ewidentny bd, ktry powinien zosta wykryty przez kompilator.
Jednak musimy mu o tym powiedzie i w tym celu posugujemy si swkiem explicit
(jawny):
explicit CIntArray(unsigned uRozmiar)
: m_uRozmiar(uRozmiar);
m_pnTablica(new int [m_uRozmiar]) { }
Gdy opatrzymy nim deklaracj konstruktora jednoargumentowanego, bdzie to znakiem,
i nie chcemy, aby wykonywa on niejawn konwersj. Po zastosowaniu tego manewru
sporny kod nie bdzie si ju kompilowa. I bardzo dobrze.
Jeeli potrzebujesz konstruktora jednoparametrowego, ktry bdzie dziaa
wycznie jako zwyky (a nie te jako konwertujcy), umie w jego deklaracji sowo
kluczowe explicit.
Jak wiemy konstruktor konwertujcy moe mie wicej argumentw, jeli ma te
parametry opcjonalne. Do takich konstruktorw rwnie mona stosowa explicit, jeli
jest to konieczne.
Operatory konwersji
Teraz poznamy drugi sposb konwersji typw - funkcje (operatory) konwertujce.
104
A take podobna do niego inicjalizacja oraz kade uycie liczby int w miejsce tablicy CIntArray.
Zaawansowane C++ 368
Stwarzamy sobie problem
Zostawmy wysz matematyk liczb zespolonych w klasie CComplex i zajmijmy si klas
CRational. Jak wiemy, reprezentowane przez ni liczby wymierne s take liczbami
rzeczywistymi. Byoby zatem dobrze, abymy mogli przekazywa je w tych miejscach,
gdzie wymagane s liczby zmiennoprzecinkowe, np.:
float abs(float x);
float sqrt(float x);
// itd.
Niestety, nie jest to moliwe. Obecnie musimy sami dzieli licznik przez mianownik, aby
otrzyma liczb typu float z typu CRational. Dlaczego jednak kompilator nie miaby
tutaj pomc? Zdefiniujmy niejawn konwersj z typu CRational do float!
W tym momencie napotkamy powany problem. Konwersja do typu CRational bya jak
najbardziej moliwa poprzez konstruktor, natomiast zamiana z typu CRational na float
nie moe by ju tak zrealizowana. Nie moemy przecie doda konstruktora
konwertujcego do klasy float, bo jest to elementarny typ podstawowy. Zreszt,
nawet jeli nasz docelowy typ byby klas, to nie zawsze byoby to moliwe. Konieczna
byaby bowiem modyfikacja definicji tej klasy, a to jest moliwe tylko dla naszych
wasnych klas.
Tak wic konstruktory konwertujce na niewiele nam si zdadz. Potrzebujemy innego
sposobu
Definiowanie operatora konwersji
T now metod jest operator konwersji. Metod w sensie dosownym - musimy bowiem
zdefiniowa go jako metod klasy CRational:
CRational::operator float()
{
return m_nLicznik / static_cast<float>(m_nMianownik);
}
Oglnie wic funkcja w postaci:
klasa::operator typ()
{
ciao_funkcji
}
definiuje sposb, w jaki dokonywna jest konwersja klasy do podanego typu. Zatem:
Operatorw konwersji moemy uywa, aby zdefiniowa niejawn konwersj typu
swojej klasy na inny, dowolny typ.
Zyskujemy to, na czym nam zaleao. Odtd moemy swobodnie przekazywa liczby
wymierne w tych miejscach, gdzie funkcje daj liczb rzeczywistych:
CRational Liczba(3, 4);
float fPierwiastek = sqrt(Liczba);
Jest to zasuga operatorw konwersji.
Operatory konwersji, w przeciwiestwie do konstruktorw, s dziedziczone i mog by
metodami wirtualnymi.
Zaawansowana obiektowo 369
Wybr odpowiedniego sposobu
Mamy wic dwa sposoby konwersji typw. Nasuwa si pytanie: ktry wybra? Pytanie to
jest zasadne, bowiem jeli w konwersji dwch typw uyjemy obu drg (konstruktor oraz
operator), to powstanie wieloznaczno. Gdy kompilator bdzie zmuszony sign po
konwersj, nie bdzie mg zdecydowa si na aden sposb i zaprotestuje.
Aby odpowiedzie na to wane pytanie, przypomnijmy, jak dziaaj obie metody
konwersji:
konstruktor konwertujcy dokonuje zamiany innego typu w obiekt naszej klasy
operator konwersji zamienia obiekt naszej klasy w obiekt innego typu
Schemat 38. Sposoby dokonywania niejawnych konwersji w C++
Wszystko zaley wic od tego, ktry z typw - rdowy, docelowy - jest klas, do ktrej
definicji mamy dostp:
jeeli jestemy w posiadaniu definicji klasy docelowej, to moemy zastosowa
konstruktor konwertujcy
jeli mamy dostp do klasy rdowej, moliwe jest zastosowanie operatora
konwersji
W przypadku gdy oba warunki s spenione (tzn. chcemy wykona konwersj z
wasnorcznie napisanej klasy do innej wasnej klasy), wybr sposobu jest w duej
mierze dowolny. Trzeba jednak pamita, e:
konstruktory nie s dziedziczone, wic w jeli chcemy napisac konwersj typu do
klasy pochodnej, potrzebujemy osobnego konstruktora w tej klasie
konstruktory nie s metodami wirtualnymi, w przeciwiestwie do operatorw
konwersji
argument konstruktora konwertujcego musi mie typ cile dopasowany do
zadeklarowanego
W sumie wic wnioski z tego s takie (czytaj: przechodzimy do sedna :D):
chcc wykona konwersj typu podstawowego (lub klasy bibliotecznej) do typu
wasnej klasy, stosujemy konstruktor konwertujcy
chcc dokona konwersji typu wasnej klasy do typu podstawowego (lub klasy
bibliotecznej), wykorzystujemy operator konwersji
definiujc konwersj midzy dwoma wasnymi klasami moemy wybra, kierujc
si innymi przesankami, jak np. wpywem dziedziczenia na konwersje czy nawet
kolejnoci definicji obu klas w pliku nagwkowym
***
Zbiorem dobrych rad odnonie stosowania rnych typw konwersji zakoczylimy
omawianie zaawansowanych aspektw konstruktorw w C++.
Zaawansowane C++ 370
Przecianie operatorw
W tym podrozdziale przyjrzymy si unikalnej dla C++, a jednoczenie wspaniaej
technice przeciania operatorw. To jedno z najwikszych osigni tego jzyka w
zakresie uatwiania programowania i uczynienia go przyjemniejszym.
Zanim jednak poznamy t cudowno, czas na krtk dygresj :) Jak ju wielokrotnie
wspomniaem, C++ jest czonkiem bardzo licznej dzisiaj rodziny jzykw obiektowych.
Takie jzyki charakteryzuje moliwo tworzenia wasnych typw danych - klas -
zawierajcych w sobie (kapsukujcych) pewne dane (pola) oraz pewne dziaania
(metody). Na tym polega OOP.
aden jzyk programowania nie moe si jednak oby bez mniej lub bardziej
rozbudowanego wachlarza typw podstawowych. W C++ mamy ich mnstwo, z czego
wikszo jest spadkiem po jego poprzedniku, jzyku C.
Z jednej strony mamy wic typy wbudowane (w C++: int, float, unsigned, itd.), a
drugiej typy definiowane przez uytkownika (struktury, klasy, unie). W jakim stopniu s
one do siebie podobne?
Pomylisz: Gupie pytanie! One przecie wcale nie s do siebie podobne. Typw
podstawowych uywamy przeciez inaczej ni klas, i na odwrt. Nie ma mowy o jakim
wikszym podobiestwie - moe poza tym, e dla wszystkich typw moemy deklarowa
zmienne i parametry funkcji No i moe jeszcze wystpuj podobne konwersje Jeeli
faktycznie tak pomylae, to nie bdziesz zdziwiony, e twrcy wielu jzykw
obiektowych take przyjli tak strategi. W jzykach Java, Object Pascal (Delphi), Visual
Basic, PHP i jeszcze wielu innych, typy definiowane przez uytkownika (klasy) s jakby
wydzielon czci jzyka. Maj niewiele punktw wsplnych z typami wbudowanymi,
poza tymi naprawd niezbdnymi, ktre sam wyliczye.
Jednak wcale nie musi tak by i C++ jest tego najlepszym przykadem. Autorzy tego
jzyka (z Bjarne Stroustrupem na czele) dyli bowiem do tego, aby definiowane przez
programist typy byy funkcjonalnie jak najbardziej zblione do typw wbudowanych. Ju
sam fakt, e moemy tworzy obiekty na dwa sposoby - jak normalne zmienne oraz
poprzez new - dobrze o tym wiadczy. Moliwo zdefiniowania konstruktorw
kopiujcych i konwersji wiadczy o tym jeszcze bardziej.
Ale ukoronowaniem tych wysikw jest obecno w C++ mechanizmu przeciania
operatorw.
Czy wic jest ten wspaniay mechanizm?
Przecianie operatorw (ang. operator overloading), zwane te ich
przeadowaniem, polega na nadawaniu operatorom nowych znacze - tak, aby mogy
by one wykorzystane w stosunku do obiektw zdefiniowanych klas.
Polega to wic na napisaniu takiego kodu, ktry sprawi, e wyraenia w rodzaju:
a = b + c
a /= d
if (b == c) { /* ... */ }
bd poprawne nie tylko wtedy, gdy a, b, c i d bd zmiennymi, nalecymi do typw
wbudowanych. Po przecieniu operatorw (tutaj: +, =, /= i ==) dla okrelonych klas
bdzie mona pisa takie wyraenia: zawierajce operatory i obiekty naszych klas. W ten
sposb zdefiniowane przez nas klasy nie bd si rniy praktycznie niczym od typw
wbudowanych.
Zaawansowana obiektowo 371
Dlaczego to jest takie cudowne? By si o tym przekona, przypomnijmy sobie
zdefiniowan ongi klas liczb wymiernych - CRational. Napisalimy sobie wtedy
funkcj, ktra zajmowaa si ich mnoeniem. Uywalimy jej w ten sposb:
CRational Liczba1(1, 2), // 1/2, czyli p :)
Liczba2(5, 1), // 5
Wynik; // zmienna na wynik
Wynik = Pomnoz(Liczba1, Liczba2);
Nie wygldao to zachwycajco, szczeglnie jeli uwiadomimy sobie, e dla typw
wbudowanych ostatnia linijka mogaby prezentowa si tak:
Wynik = Liczba1 * Liczba2;
Nie do, e krcej, to jeszcze adniej Czemu my tak nie moemy?!
Ale tak, wanie moemy! Przecianie operatorw pozwala nam na to! Znajc t
technik, moemy zdefiniowac nowe znaczenie dla operator mnoenia, czyli *. Nauczymy
go pracy z liczbami wymiernymi - obiektami naszej klasy CRational - i od tego momentu
pokazane wyej mnoenie bdzie dla nich poprawne! Co wicej, bdzie dziaao zgodnie
z naszymi oczekiwaniami: tak, jak funkcja Pomnoz(). Czy to nie pikne?
Na takie wspaniaoci pozwala nam przecianie operatorw. Na co wic jeszcze czekamy
- zobaczmy, jak to si robi! Hola, nie tak prdko! Jak sama nazwa wskazuje, technika
ta dotyczy operatorw, a dokadniej: wyposaania ich w nowe znaczenia. Zanim si za to
zabierzemy, warto byoby zna przedmiot naszych manipulacji. Powinnimy zatem
przyjrze si operatorom w C++: ich rodzajom, wbudowanej funkcjonalnoci oraz innym
waciwociom.
I to wanie zrobimy najpierw. Tylko nie narzekaj :P
Cechy operatorw
Obok sw kluczowych i typw, operatory s podstawowymi elementami kadego jzyka
programowania wysokiego poziomu. Przypomnijmy sobie, czym jest operator.
Operator to jeden lub kilka znakw (zazwyczaj niebdcych literami), ktre maj
specjalne znaczenie w jzyku programowania.
Dotychczas uywalimy bardzo wielu operatorw - niemal wszystkich, jakie wystpuj w
C++ - ale dotd nie zajlimy si nimi caociowo. Poznae wprawdzie takie pojcia jak
operatory unarne, binarne, priorytety, jednak teraz bdzie zasadne ich powtrzenie.
Zbierzmy wic tutaj wszystkie cechy operatorw wystpujcych w C++.
Liczba argumentw
Operator sam w sobie nie moe wykonywa adnej czynnoci (to rni go od funkcji),
gdy potrzebuje jakich parametrw. W tym przypadku mwimy zwykle o argumentach
operatora - operandach.
Operatory dziel si z grubsza na dwie due grupy, jeeli chodzi o liczb swoich
argumentw. S to operatory jedno- i dwuargumentowe. W C++ mamy jeszcze operator
warunkowy ?:, uznawany za ternarny (trjargumentowy), ale jest on wyjtkiem, ktrym
nie naley zaprzta sobie gowy.
Zaawansowane C++ 372
Operatory jednoargumentowe
Te operatory fachowo nazywa si unarnymi (ang. unary operators). Stanowi one
cakiem liczn rodzin, ktra charakteryzuje si jednym: kady jej czonek wymaga do
dziaania jednego argumentu. Std nazwa tego rodzaju operatorw.
Najbardziej znanym operatorem unarnym (nawet dla tych, ktrzy nie maj pojcia o
programowaniu!) jest zwyky minus. Formalnie nazywa si go operatorem negacji albo
zmiany znaku, a dziaa on w ten sposb, e zmienia jaka liczb na liczb do niej
przeciwn:
int nA = 5;
int nB = -nA; // nB ma warto -5 (a nA nadal 5)
Podobnie dziaaj operatory ! i ~, z tym e operuj one (odpowiednio): na wyraeniach
logicznych i na cigach bitw. Istniej te operatory jednoargumentowane o zupenie
innej funkcjonalnoci; wszystkie je przypomnimy sobie w nastpnej sekcji.
Operatory dwuargumentowe
Jak sama nazwa wskazuje, te operatory przyjmuj po dwa argumenty. Nazywamy je
binarnymi (ang. binary operators). Nie ma to nic wsplnego z binarn reprezentacj
danych, lecz po prostu z iloci operandw.
Typowymi operatorami dwuargumentowymi s operatory arytmetyczne, czyli popularne
znaki dziaa:
int nA = 8, nB = -2, nC;
nC = nA + nB; // 6
nC = nA - nB; // 10
nC = nA * nB; // -16
nC = nA / nB; // -4
Mamy te operatory logiczne oraz bitowe, Warto wspomnie (o czym bdziemy jeszcze
bardzo szeroko mwi), e przypisanie (=) to take operator dwuargumentowy, do
specyficzny zreszt.
Priorytet
Operatory mog wystpowa w zoonych wyraeniach, a ich argumenty mog pokrywa
si. Oto prosty przykad:
int nA = nB * 4 + 18 / nC - nD % 3;
Zapewne wiesz, e w takiej sytuacji kompilator kieruje si priorytetami operatorw
(ang. operators precedence), aby rozstrzygn problem. Owe priorytety to nic innego,
jak swoista kolejno dziaa. Rni si ona od tej znanej z matematyki tylko tym, e w
C++ mamy take inne operatory ni arytmetyczne.
Dla znakw +, -, *, /, % priorytety s aczkolwiek dokadnie takie, jakich nauczylimy si
w szkole. Wyraenia zawierajce te operatory moemy wic pisa bez pomocy nawiasw.
Jeeli jednak s one skomplikowane, albo uywamy w nich take innych rodzajw
operatorw, wwczas konieczne naley pomaga sobie nawiasami. Lepiej przecie
postawi po kilka znakw wicej ni co chwila siga do stosownej tabelki pierwszestwa.
Zaawansowana obiektowo 373
czno
Gdy w wyraeniu pojawi si obok siebie kilka operatorw tego samego rodzaju, maj one
oczywicie ten sam priorytet. Trzeba jednak nadal rozstrzygn, w jakiej kolejnoci
dziaania bd wykonywane.
Tutaj pomaga czno operatorw (ang. operators associativity). Okrela ona, od
ktrej strony bd obliczane wyraenia (lub ich fragmenty) z ssiedztwem operatorw o
tych samych priorytetach. Mamy dwa rodzaje cznoci:
czno lewostronna (ang. left-to-right associativity), ktra rozpoczyna
obliczenia od lewej strony i wykorzystuje czstkowe wyniki jako lewostronne
argumenty dla kolejnych operatorw
czno prawostronna (ang. right-to-left associativity) - tutaj obliczenia s
wykonywane, poczynajc od prawej strony. Czciowe wyniki s nastpnie
uywane jako prawostronne argumenty kolejnych operatorw
Najlepiej zilustrowa to na przykadzie. Jeeli mamy takie oto wyraenie:
nA + nB + nC + nD + nE + nF + nG + nH
to oczywicie priorytety wszystkich operatorw s te same. Zaczyna dominowa czno,
ktra w przypadku operatorw arytmetycznych (oraz im podobnych, jak bitowe, logiczne
i relacyjne) jest lewostronna. To naturalne, po przecie takie obliczenia rwnie
przeprowadzalibymy od lewej do prawej.
Kompilator bdzie wic oblicza powysze wyraenie w ten sposb:
((((((nA + nB) + nC) + nD) + nE) + nF) + nG) + nH
Zauwamy, e akurat w przypadku plusa czno nie ma znaczenia, bo dodawanie jest
przecie przemienne. Gdyby jednak chodzio o odejmowanie czy dzielenie, wwczas
byoby to bardzo wane.
czno prawostronna dotyczy na przykad operatora przypisania:
nA = nB = nC = nD = nE = nF
Innymi sowy, powysze wyraenie zostanie potraktowane tak:
nA = (nB = (nC = (nD = (nE = nF))))
Oznacza to, e kompilator wykona najpierw skrajnie prawe przypisanie, a zwrcon przez
to wyraenie warto (rwn wartoci przypisywanej) wykorzysta w kolejnym
przypisaniu, i tak dalej. W sumie wic wszystkie zmienne bd potem rwne zmiennej
nF.
Operatory w C++
Jzyk C++ posiada cae multum rnych operatorw. Pod tym wzgldem jest chyba
rekordzist wrd wszystkich jzykw programowania. wiadczy to zarwno o jego
wielkich moliwociach, jak i sporej elastycznoci.
Co ciekawe, dotd praktycznie nie ma jednoznacznej definicji operatora w tym jzyku, a
w wielu rdach mona znale nieco rnice si midzy sob zestawy operatorw. S
to jednak gwnie niuanse, ktrych rozstrzyganie dla przecitnego programisty nie jest
wcale istotne.
Zaawansowane C++ 374
W tej sekcji powtrzymy sobie i uzupenimy wiadomoci na temat wszystkich operatorw
C++ - przynajmniej tych, co do ktrych nie ma wtpliwoci, e faktycznie s
operatorami. Podzielimy je sobie na kilka kategorii.
Operatory arytmetyczne
Ju na samym pocztku zetknlimy si z operatorami arytmetycznymi. Nic dziwnego, to
przecie najprostszy i najbardziej naturalny rodzaj operatorw. Znaj go wszyscy
absolwenci przedszkola.
Unarne operatory arytmetyczne
Mamy dwa podstawowe jednoargumentowe operatory arytmetyczne:
operator zachowania znaku, czyli +. On praktycznie nie robi nic - zachowuje
znak liczby, przy ktrej stoi. Obecny w C++ chyba tylko dla zgodnoci z zasadami
matematyki
operator zmiany znaku, czyli -. Zamienia liczb na przeciwn, zupenie tak jak w
arytmetyce
Troch przykadw:
int nA = 7
int nB = +nA; // 7
int nB = -NA; // -7
Myl, e jest to na tyle oczywiste, e nie wymaga dalszych komentarzy.
Inkrementacja i dekrementacja
Specyficzne dla C++ s operatory inkrementacji i dekrementacji. W odrnieniu od
wikszoci operatorw, modyfikuj one swj argument. Dokadniej mwic, dodaj
one (inkrementacja) lub odejmuj (dekrementacja) jedynk do/od swego operandu.
Operatorem inkrementacji jest ++, za dekrementacji --. Oto przykad:
int nX = 9;
++nX; // teraz nX == 10
--nX; // teraz znowu nX == 9
Powyszy kod mona te zapisa jako:
nX++;
nY++;
Jeeli ignorujemy warto zwracan przez te operatory, to uycie ktrejkolwiek wersji
(zwanej, jak wiesz, prein/dekrementacj oraz postin/dekrementacj) nie sprawa rnicy
- przynajmniej dla typw podstawowych.
Gdy natomiast zapisujemy gdzie zwracan warto, to powinnimy pamita o rnicy
midzy znaczeniem operatorw w obu miejscach (na pocztku i na kocu zmiennej).
Mwilimy ju o tym, ale przypomn jeszcze raz:
Prein/dekrementacja zwraca warto ju zwikszon (zmniejszon) o 1.
Postin/dekrementacja zwraca oryginaln warto.
Wariant postfiksowy jest generalnie bardziej kosztowny, poniewa wymaga
przygotowania tymczasowego obiektu, w ktrym zostanie zachowana pierwotna warto
w celu jej pniejszego zwrotu. Dla typw podstawowych to kwestia kilku bajtw, ale dla
klas zdefiniowanych przez uytkownika (ktre mog przecia oba operatory - czym si
rzecz jasna zajmiemy za momencik) moe to by spora rnica.
Zaawansowana obiektowo 375
Binarne operatory arytmetyczne
Przypomnijmy, e w C++ mamy pi takich operatorw, zwanych popularnie znakami
dziaa:
operator dodawania - plus (+). Dodaje dwie liczby do siebie
operator odejmowania - minus (-). Zwraca wynik odejmowania drugiego
argumentu od pierwszego
operator mnoenia - gwiazdka (*). Mnoy oba argmenty
operator dzielenia - slash (/). W zalenoci od typu swoich operandw moe albo
wykonywa dzielenie cakowitoliczbowe (gdy oba argumenty s liczbami
cakowitymi), albo zmiennoprzecinkowe
operator reszty z dzielenia, czyli %. Zwraca reszt z dzielenia podanych liczb
Znowu popatrzmy na kilka przykadw:
int nA = 9, nB = 4, nX;
float fX;
nX = nA + nB; // 13
nX = nA - nB; // 5
nX = nA * nB; // 36
nX = nA / nB; // 2
fX = nA / static_cast<float>(nB); // 2.25f
nX = nA % nB; // 1
Ponownie nie ma tu nic nieoczekiwanego.
Operatory bitowe
Przedstawione wyej operatory arytmetyczne dziaaj na liczbach na zasadach, do jakich
przyzwyczaia nas matematyka. Nie ma w tym przypadku znaczenia, e operacje
przeprowadzane s na komputerze. Nie ma te znaczenia wewntrzna reprezentacja
liczb.
Jak wiemy, komputery przechowuj dane w postaci cigw zer i jedynek, zwanych
bitami. Pojedyncze bity mog przechowywa tylko elementarn informacj - 0 (bit
ustawiony) lub 1 (bit nieustawiony). Aby przedstawia bardziej zoone dane - choby
liczby - naley bity czy ze sob. Powstaj w ten sposb wektory bitowe, cigi bitw
(ang. bitsets) lub sowa (ang. words). S po prostu sekwencje zer i jedynek.
Do operacji na wektorach bitw C++ posiada sze operatorw. Obecnie nie s one tak
czsto uywane jak na przykad w czasach C, ale nadal s bardzo przydatne. Omwi je
tu pokrtce.
O wiele obszerniejsze omwienie tych operatorw, wraz z zastosowaniami, znajdziesz w
Dodatku C, Manipulacje bitami.
Operacje logiczno-bitowe
Cztery operatory: ~, &, | i ^ wykonuj na bitach operacje zblione do logicznych, gdzie
bit ustawiony (1) odgrywa rol wyraenia prawdziwego, za nieustawiony (0) -
faszywego. Oto te operatory:
negacja bitowa (operator ~) zmienia w caym cigu (zwykle liczbie) wszystkie
bity na przeciwne. Ustawione zmieniaj si na nieustawione i odwrotnie
koniunkcja bitowa (operator &) porwnuje ze sob odpowiadajce bity dwch
sw: tam, gdzie napotka na dwie jedynki, wypisuje do wyniku take jedynk; w
przeciwnym wypadku zero
Zaawansowane C++ 376
alternatywa bitowa (operator |) rwnie dziaa na dwch sowach. Porwnujc
ich kolejne bity, zwraca w bicie wyniku zero, jeeli stwierdzi w operandach dwa
nieustawione bity oraz jedynk w przeciwnym wypadku
bitowa rnica symetryczna (operator ^) porwnuje bity sw i zwraca 1, jeeli
s rne i 0, gdy s sobie rwne
Operator ~ jest jednoargumentowy (unarny), za pozostae dwa s binarne - i wcale nie
dlatego, e pracuj w systemie dwjkowym :)
Przesunicie bitowe
Mamy te dwa operatory przesunicia bitowego (ang. bitwise shift). Jest to:
przesunicie w lewo (operator <<). Przesuwa on bity w lew stron sowa o
podan liczb miejsc
przesunicie w prawo (operator >>) dziaa analogicznie, tylko e przesuwa bity
w praw stron sowa
Z obu operatorw korzystamy podobnie, tj. w ten sposb:
sowo << ile_miejsc
sowo >> ile_miejsc
Oto kilka przykadw - dla uproszczenia z liczbami zapisanymi binarnie (niestety, w C++
nie mona tego zrobi):
00010010 << 3 // 10010000
1111000 >> 4 // 00001111
00111100 << 5 // 10000000
Jak wida, bity ktre wyjedaj w wyniku przesunicia poza granic sowa s tracone.
Pustki s natomiast wypeniane zerami.
Operatory strumieniowe
Czytajc ten akapit na pewno pomylae: Jakie operatory bitowe?! Przecie to s
strzaki, ktrych uywamy razem ze strumieniami wejcia i wyjcia! Tak, to rwnie
prawda - ale to tylko jedna jej strona.
Faktem jest, e << i >> to przede wszystkim operatory przesunicia bitowego. Nie
przeszkadza to jednak, aby miay one take inne znaczenie - co wicej, maj je one tylko
w odniesieniu do strumieni. W sumie wic peni one w C++ a dwie funkcje.
Czy domylasz si, dlaczego? Ale tak, wanie tak - operatory te zostay przecione
przez twrcw Biblioteki Standardowej C++. Posiadaj one dodatkow funkcjonalno,
ktra pozwala na ich uywanie razem z obiektami cout i cin
105
. W odniesieniu do samych
liczb nadal jednak s one operatorami przesunicia bitowego.
Nieco wicej informacji o tych operatorach otrzymasz przy okazji omawiania strumieni
STL. Tam te nauczysz si przecia je dla swoich wasnych klas - tak, aby ich obiekty
mona byo zapisywa do strumieni i odczytywa z nich w identyczny sposb, jak typy
wbudowane.
Operatory porwnania
Bardzo wanym rodzaje operatorw s operatory porwnania, czyli znaki: < (mniejszy), >
(wikszy), <= (mniejszy lub rwny), >= (wikszy lub rwny), == (rwny) oraz != (rny).
105
Rwnie clog, cerr oraz wszystkimi innymi obiektami, wywodzcymi si od klas istream i ostream oraz ich
pochodnych. Po wicej informacji odsyam do rozdziau o strumieniach Biblioteki Standardowej.
Zaawansowana obiektowo 377
O tych operatorach wiemy w zasadzie wszystko, bo uywamy ich nieustannie. O tym, jak
dziaaj, powiedzielimy sobie zreszt bardzo wczenie.
Zwrc jeszcze tylko uwag, aby nie myli operatora rwnoci (==) z operatorem
przypisania (=). Omykowe uycie tego drugiego w miejsce pierwszego nie zostanie
bowiem oprotestowane przez kompilator (co najwyej wygeneruje on ostrzeenie).
Dlaczego tak jest - wyjani przy okazji operatrw przypisania.
Operatory logiczne
Te operatory su do czenia wyrae logicznych (true lub false) w zoone warunki.
Takie warunki moemy potem wykorzysta z instrukcjach if oraz ptlach, co zreszt
niejednokrotnie robilimy.
W C++ mamy trzy operatory logiczne, bdce odpowiednikami pewnych operatorw
bitowych. Rnica polega jednak na tym, e operatory logiczne dziaaj na wartociach
liczb (lub wyrae logicznych: faszywe oznacza 0, za prawdziwe - 1) za bitowe - na
wartociach bitw.
Oto te trzy operatory:
negacja (zaprzeczenie, operator !) powoduje zamian prawdy (1) na fasz (0)
koniunkcja (iloczyn logiczny, operator &&) dwch wyrae zwraca prawd tylko
wwczas, gdy oba jej argumenty s prawdziwe
alternatywa (suma logiczna, operator ||) jest prawdziwa, gdy cho jeden z jej
argumentw jest prawdziwy (rny od zera)
Warto zapamita, e w wyraeniach zawierajcych operatory && i || wykonywanych jest
tylko tyle oblicze, ile jest koniecznych do zdeterminowania wartoci warunkowych.
Przykadowo, w poniszym kodzie:
int nZmienna;
std::cin >> nZmienna;
if (nZmienna >= 1 && nZmienna <= 10) { /* ... */ }
jeeli stwierdzona zostanie falszywo pierwszej czci koniunkcji (nZmienna >= 1), to
druga nie bdzie ju sprawdzana i cay warunek uznany zostanie za faszywy. Podobnie
dzieje si przy alternatywie, ktrej pierwszy argument jest prawdziwy - wwczas cae
wyraenie rwnie reprezentuje prawd.
Argumenty operatorw logicznych s wic zawsze obliczane od lewej do prawej.
Wrd operatorw nie ma rnicy symetrycznej, zwanej alternatyw wykluczajc
(ang. XOR - eXclusive OR). Mona j jednak atwo uzyska, wykorzystujc tosamo:
( ) a b a b
co w przeoeniu na C++ wyglda tak:
if (!(a == b)) { /* ... */ } // a i b to wyraenia logiczne
Operatory przypisania
Kolejn grup stanowi operatory przypisania. C++ ma ich kilkanacie, cho wiemy, e
tak naprawd tylko jeden jest do szczcia potrzebny. Pozostae stworzono dla wygody
programisty, jak zreszt wiele mechanizmw w C++.
Popatrzmy wic na operatory przypisania.
Zaawansowane C++ 378
Zwyky operator przypisania
Operator przypisania (ang. assignment operator) ma posta pojedynczego znaku rwna
si (=). Doskonale te wiemy, jak si go uywa:
int nX;
nX = 7;
Po wykonaniu tego kodu, zmienna nX bdzie mia warto 7.
L-warto i r-warto
Zauwamy, e odwrotne przypisanie:
7 = nX; // le!
jest niepoprawne. Nie moemy nic przypisa do sidemki, bo ona nie zajmuje adnej
komrki w pamici - w przeciwiestwie do zmiennej, jak np. nX.
Zarwno 7, jak i nX, s jednak poprawnymi wyraeniami jzyka C++. Widzimy
aczkolwiek, e rni si pod wzgldem wsppracy z przypisaniem. nX moe by celem
przypisania, za 7 - nie.
Mwimy, e nX jest l-wartoci, za 7 - r-wartoci lub p-wartoci.
L-warto (ang. l-value) jest wyraeniem mogcym wystpi po lewej stronie
operatora przypisania - std ich nazwa.
R-warto (ang. r-value), po polsku zwana p-wartoci, moe wystpi tylko po
prawej stronie operatora przypisania.
Zauwamy, e nic nie stoi na przeszkodzie, aby nX pojawio si po prawej stronie
operatora przypisania:
int nY;
nY = nX;
Jest tak, poniewa:
Kada l-warto jest jednoczenie r-wartoci (p-wartoci) - lecz nie odwrotnie!
Domylasz si pewnie, e w C++ kade wyraenie jest r-wartoci, poniewa
reprezentuje jakie dane. L-wartociami s natomiast te wyraenia, ktre:
odpowiadaj komrkom pamici operacyjnej
nie s oznaczone jako stae (const)
Najbardziej typowymi rodzajami l-wartoci s wic:
zmienne wszystkich typw niezadeklarowane jako const
wskaniki do powyszych zmiennych, wobec ktrych stosujemy operator
dereferencji, czyli gwiazdk (*)
niestae referencje do tyche zmiennych
elementy niestaych tablic
niestae pola klas, struktur i unii, ktre podpadaj pod jeden z powyszych
punktw i nie wystpuj w ciele staych metod
106
106
Wyjtkiem s pola oznaczone sowem mutable, ktre zawsze mog by modyfkowane.
Zaawansowana obiektowo 379
R-wartoci to oczywicie te, jak i wszystkie inne wyraenia.
Rezultat przypisania
Wyraeniem jest take samo przypisanie, gdy samo w sobie reprezentuje pewn
warto:
std::cout << (nX = 5);
Ta linijka kodu wyprodukuje rezultat:
5
co pozwala nam uglni, i:
Rezultatem przypisania jest przypisywana warto.
Ten fakt powoduje, e w C++ moliwe s, niespotykane w innych jzykach, wielokrotne
przypisania:
nA = nB = nC = nD = nE;
Poniewa operator(y) przypisania maj czno prawostronn, wic ten wiersz zostanie
obliczony jako:
nA = (nB = (nC = (nD = nE)));
Innymi sowy, nE zostanie przypisane do nD. Nastpnie rezultat tego przypisania (czyli
nE, bo to byo przypisywane) zostanie przypisany do nC. To take wyprodukuje rezultat -
i to ten sam, nE - ktry zostanie przypisany nB. To przypisanie rwnie zwrci ten sam
wynik, ktry zostanie wreszcie umieszczony w nA. W ten wic sposb wszystkie zmienne
bd miay ostatecznie t sam warto, co nE.
T technik moemy wykona tyle przypisa naraz, ile tylko sobie yczymy.
Uwaga na przypisanie w miejscu rwnoci
Niestety, traktowanie przypisania jako wyraenia ma te swoj ciemn stron. Bardzo
atwo jest umieci je omykowo w warunku if lub ptli zamiast operatora ==, np.:
while (nA = 5)
std::cin >> nA;
Jeeli nasz kompilator jest lekkoduchem, to moe nas nie ostrzec przed
niebezpieczestwem tej ptli. A zagroenie jest spore, bo jest nic innego, jak ptla
nieskoczona, Podobno komputer Cray wykonaby j w dwie sekundy - jeeli chcesz,
moesz sprawdzi, ile zajmie to twojej maszynie ;D Lepiej jednak zaradzi powstaemu
problemowi.
Jak on jednak powstaje? Ot sprawa jest do prosta, a wszystkiemu winien warunek
ptli. Jest to przecie przypisanie - przypisanie wartoci 5 do zmiennej nA. Jako test
logiczny wykorzystywana jest warto tego przypisania - czyli pitka. Pi jest
oczywicie rne od zera, zatem zostanie uznane za warunek prawdziwy. Tak oto ptla
si zaptla i zaciska na szyi biednego programisty.
Moemy si koci, e to wina C++, ktry nie do, e uznaje liczby cakowite (jak 5) za
wyraenia logiczne, to jeszcze pozwala na wykonywanie przypisania w warunkach ifw i
ptli. Moliwoci te zostay jednak dopuszczone z uzasadnionych wzgldw (praca ze
wskanikami), wic wcale niewykluczone, e kiedy je docenimy. Niezalenie od tego, czy
Zaawansowane C++ 380
bdziemy wiadomie wykonywa przypisania w podobnych sytuacjach, musimy pamita,
e:
Naley zwraca baczn uwag na kade przypisanie wystpujce w warunku instrukcji if
lub ptli. Moe to by bowiem niedosze porwnanie.
Zaleca si, aby opatrywa stosownym komentarzem kade zamierzone uycie
przypisania w tych newralgicznych miejscach. Dziki temu unikniemy nieporozumie z
kompilatorem, innymi programistami i samym sob!
Zoone operatory przypisania
Dla wygody programisty C++ posiada jeszcze dziesi innych operatorw przypisania. S
one po prostu krtszym zapisem czsto stosowanych instrukcji. Ich posta i rozwinicia
przedstawia to oto tabelka:
przypisanie rozwinicie
a += b a = a + b
a -= b a = a - b
a *= b a = a * b
a /= b a = a / b
a %= b a = a % b
a &= b a = a & b
a |= b a = a | b
a ^= b a = a ^ b
a <<= b a = a << b
a >>= b a = a >> b
Tabela 17. Zoone operatory przypisania w C++
Rozwinicie wziem w cudzysw, poniewa nie jest tak, e jaki mechanizm w rodzaju
makrodefinicji zamienia te skrcone wyraenia do ich penych form. O nie, one s
kompilowane w tej postaci. Ma to taki skutek, e wyraenie po lewej stronie operatora
jest obliczane jeden raz. W wersji rozwinitej byoby natomiast obliczane dwa razy.
Podobna zasada obowizuje te w operatorach pre/postin/dekrementacji.
Jest to te realizacja bardziej fundamentalnej reguy, ktra mwi, e skadniki kadego
wyraenia s obliczane tylko raz.
Operatory wskanikowe
Wskaniki byy ongi kluczow cech jzyka C, a i w C++ nie straciy wiele ze swojego
znaczenia. Do ich obsugi mamy w naszym ulubionym jzyku trzy operatory.
Pobranie adresu
Jednoargumentowy operator & suy do pobrania adresu obiektu, przy ktrym stoi. Oto
przykad:
int nZmienna;
int* pnWskaznik = &nZmienna;
Argument tego operatora musi by l-wartoci. To raczej oczywiste, bo przecie musi
ona rezydowa w jakim miejscu pamici. Inaczej niemoliwe byoby pobranie adresu
tego miejsca. Typowo operandem dla & jest zmienna lub funkcja.
Zaawansowana obiektowo 381
Dostp do pamici poprzez wskanik
Do obszaru pamici, do ktrego posiadamy wskanik, moemy odnie si na kilka
sposobw. Dokadnie: na dwa.
Dereferencja
Najprostszym i najczciej stosowanym sposobem jest dereferencja:
int nZmienna;
int* pnWskaznik = &nZmienna;
*pnWskaznik = 42;
Odpowiada za ni jednoargumentowy operator *, zwany operatorem dereferencji lub
adresowania poredniego. Pozwala on na dostp do miejsca w pamici, ktremu
odpowiada wskanik. Operator ten wykorzystuje ponadto typ wskanika, co gwarantuje,
e odczytana zostanie waciwa ilo bajtw. Dla int* bdzie to sizeof(int), zatem
*pnWskaznik reprezetuje u nas liczb cakowit.
To, czy *wskanik jest l-wartoci, czy nie, zaley od staoci wskanika. Jeeli jest to
stay wskanik (const typ*), wwczas nie moemy modyfikowa pokazywanej przeze
pamici. Mamy wic do czynienia z r-wartoci. W pozostaych przypadkach mamy l-
warto.
Indeksowanie
Jeeli wskanik pokazuje na tablic, to moemy dosta si do jej kolejnych elementw za
pomoc operatora indeksowania (ang. subscript operator) - nawiasw kwadratowych
[].
Oto zupenie banalny przykad:
std::string aBajka[3];
aBajka[0] = "Dawno, dawno temu, ...";
aBajka[1] = "w odleglej galaktyce...";
aBajka[2] = "zylo sobie siedmiu kransoludkow...";
Jeeli zapytasz A gdzie tu wskanik?, to najpierw udam, e tego nie syszaem i pozwol
ci na chwil zastanowienia. A jeli nadal bdziesz si upiera, e adnego wskanika tu
nie ma, to bd zmuszony naoy na ciebie wyrok powtrnego przeczytania rozdziau o
wskanikach. Chyba tego nie chcesz? ;-)
Wskanikiem jest tu oczywicie aBajka - jaka nazwa tablicy wskazuje na jej pierwszy
element. W zasadzie wic mona dokona jego dereferencji i dosta si do tego
elementu:
*aBajka = "Dawno, dawno temu, ...";
Przesuwajc wskanik przy pomocy dodawania mona te dosta si do pozostaej czci
tablicy:
*(aBajka + 1) = "w odleglej galaktyce...";
*(aBajka + 2) = "zylo sobie siedmiu kransoludkow...";
Taki zapis jest jednak do kopotliwy w interpretacji - cho koniecznie trzeba go zna
(przydaje si przy iteratorach STL). C++ ma wygodniejszy sposb dostepu do elementw
tablicy o danym indeksie - jest to wanie operator indeksowania.
Zaawansowane C++ 382
Na koniec musz jeszcze przypomnie, e wyraenie:
tablica[i]
odpowiada (i-1)-emu elementowi tablicy. A to dlatego, e:
W C++ elementy tablic (oraz acuchw znakw) liczymy od zera.
Skoro ju tak si powtarzam, to przypomn jeszcze, e:
W n-elementowej tablicy nie istnieje element o indeksie n. Prba odwoania si do
niego spowoduje bd ochrony pamici.
Zasada ta nie dotyczy aczkolwiek acuchw znakw, gdzie n-ty element to zawsze znak
o kodzie 0 ('\0'). Jest to zaszo zakonserwowana w czasach C, ktra przetrwaa do
dzi.
Operatory pamici
Mamy w C++ kilka operatorw zajmujcych si pamici. Jedne su do jej alokacji,
drugie do zwalniania, a jeszcze inne do pobierania rozmiaru typw i obiektw.
Alokacja pamici
Alokacja pamici to przydzielenie jej okrelonej iloci dla programu, by ten mg j
wykorzysta do wasnych celw. Pozwala to dynamicznie tworzy zmienne i tablice.
new
new jest przeznaczony do dynamicznego tworzenia zmiennych. Obiekty stworzone przy
pomocy tego operatora s tworzone na stercie, a nie na stosie, zatem nie znikaj po
opuszczeniu swego zakresu. Tak naprawd to w ogle nie stosuje si do nich pojcie
zasigu.
Tworzenie obiektw poprzez new jest banalnie proste:
float pfZmienna = new float;
Oczywicie nie ma zbyt wielkiego sensu tworzenie zmiennych typw podstawowych czy
nawet prostych klas. Jeeli jednak mamy do czynienia z duymi obiektami, ktre musz
istnie przez duszy czas i by dostpne w wielu miejscach programu, wtedy musimy
tworzy je dynamicznie poprzez new.
W przypadku kreowania obiektw klas, new dba o prawidowe wywoanie konstrukturw,
wic nie trzeba si tym martwi.
new[]
Wersj operatora new, ktra suy do alokowania tablic, nazywam new[], aby w ten
sposb podkreli jej zwizek z delete[].
new[] potrafi alokowa tablice dynamiczne po podanym rozmiarze. Aby uy tej
moliwoci po nazwie docelowego typu okrelamy wymiary podanej tablicy, np.:
float** matMacierz4x4 = new float [4][4];
Zaawansowana obiektowo 383
W wyniku dostajemy odpowiedni wskanik lub ewentualnie wskanik do wskanika (do
wskanika do wskanika itd. - zalenie od liczby wymiarw), ktry moemy zachowa w
zmiennej okrelonego typu.
Do powstaej tablicy odwoujemy si tak samo, jak do tablic statycznych:
for (unsigned i = 0; i < 4; ++i)
for (unsigned j = 0; j < 4; ++j)
matMacierz4x4[i][j] = (i == j ? 1.0f : 0.0f);
Dynamiczna tablica istnieje jednak na stercie, wic tak samo jak wszystkie obiekty
tworzone w czasie dziaania programu nie podlega reguom zasigu.
Zwalnianie pamici
Pami zaalokowana przy pomocy new i new[] musi zosta zwolniona przy pomocy
odpowiadajcych im operatorw delete i delete[]. Wiesz doskonale, e w przeciwnym
razie dojdzie do gronego bdu wycieku pamici.
delete
Za pomoc delete niszczymy pami zaalokowan przez new. Dla operatora tego naley
poda wskanik na tene blok pamici, np.:
delete pfZmienna;
delete zapewnia wywoanie destruktora klasy, jeeli takowy jest konieczny. Destruktor
taki moe by wizany wczenie (jak zwyka metoda) lub pno (jak metoda wirtualna) -
ten drugi sposb jest zalecany, jeeli chcemy korzysta z dobrodziejstw polimorfizmu.
delete[]
Analogicznie, delete[] suy do zwalniania dynamicznych tablic. Nie musimy podawa
rozmiaru takiej tablicy, gdy j niszczymy - wystarczy tylko wskanik:
delete[] matMacierz4x4;
Koniecznie pamitajmy, aby nie myli obu postaci operatora delete[] - w szczeglnoci
nie mona stosowa delete do zwalniania pamici przydzielonej przez new[].
Operator sizeof
sizeof pozwala na pobranie rozmiaru obiektu lub typu:
int nZmienna;
if (sizeof(nZmienna) != sizeof(int))
std::cout << "Chyba mamy zepsuty kompilator :D";
Jest to operator czasu kompilacji, wic nie moe korzysta z informacji uzyskanych w
czasie dziaania programu. W szczeglnoci, nie moe pobra rozmiaru dynamicznej
tablicy - nawet mimo takich prob:
int* pnTablica = new int [5];
std::cout << sizeof(pnTablica); // to samo co sizeof(int*)
std::cout << sizeof(*pnTablica); // to samo co sizeof(int)
Taki rozmiar trzeba po prostu zapisa gdzie po alokacji tablicy.
sizeof zwraca warto nalec do predefiniownego typu size_t. Zwykle jest to liczba
bez znaku lub bardzo dua liczba ze znakiem.
Zaawansowane C++ 384
Ciekawostka: operator __alignof
W Visual C++ istnieje jeszcze podobny do sizeof operator __alignof. Uywamy go w
ten sam sposb, podajc mu zmienn lub typ. W wyniku zwraca on tzw. wyrwnanie
(ang. alignment) danego typu danych. Jest to liczba, ktra okrela sposb organizacji
pamici dla danego typu danych. Przykadowo, jeeli wyrwnywanie wynosi 8, to znaczy
to, i obiekty tego typu s wyrwnane w pamici do wielokrotnoci omiu bajtw (ich
adresy s wielokrotnoci omiu).
Wyrwnanie sprawia rzecz jasna, e dane zajmuj w pamici nieco wicej miejsca ni
faktycznie mogyby. Zyskujemy jednak szybciej, poniewa porcje pamici wyrwnane do
cakowitych potg dwjki (a takie jest zawsze wyrwnanie) s przetwarzane szybciej.
Wyrwnanie mona kontrolowa poprzez __declspec(align(liczba)). Np. ponisza
struktura:
__declspec(align(16)) struct FOO { int nA, nB; };
bdzie tworzy zmienne zajmujce w pamici fragmenty po 16 bajtw, cho jej faktyczny
rozmiar jest dwa razy mniejszy
107
.
Polecajc wyrwnywanie do 1 bajta okrelimy praktyczny jego brak:
#define PACKED __declspec(align(1))
Typy danych opatrzone tak deklaracj bd wic ciasno upakowane w pamici. Moe to
da pewn jej oszczdno, ale zazwyczaj spadek prdkoci dostpu do danych nie jest
tego wart.
Operatory typw
Istniej jzyki programowania, ktre cakiem dobrze radz sobie bez posiadania cile
zarysowanych typw danych. C++ do nich nie naley: w nim typ jest spraw bardzo
wan, a do pracy z nim oddelegowano kilka specjalnych operatorw.
Operatory rzutowania
Rzutowanie jest zmian typu wartoci, czyli jej konwersj. Mamy par operatorw, ktre
zajmuj si tym zadaniem i robi to w rny sposb.
Wrd nich s tak zwane cztery nowe operatory, o skadni:
okrelenie_cast<typ_docelowy>(wyraenie)
To wanie one s zalecane do uywania we wszystkich sytuacjach, wymagajcych
rzutowania. C++ zachowuje aczkolwiek take star form rzutowania, znan z C.
static_cast
Ten operator moe by wykorzystywany do wikszoci konwersji, jakie zdarza si
przeprowadza w C++. Nie oznacza to jednak, e pozwala on na wszystko:
Poprawno rzutowania static_cast jest sprawdzana w czasie kompilacji programu.
static_cast mona uywa np. do:
konwersji midzy typami numerycznymi
rzutowania liczby na typ wyliczeniowy (enum)
107
Jeeli int ma 4 bajty dugoci, a tak jest na kadej platformie 32-bitowej.
Zaawansowana obiektowo 385
rzutowania wskanikw do klas zwizanych relacj dziedziczenia
Jeeli chodzi o ostatnie zastosowanie, to naley pamita, e tylko konwersja wskanika
na obiekt klasy pochodnej do wskanika na obiekt klasy bazowej jest zawsze bezpieczna.
W odwrotnym przypadku trzeba by pewnym co do wykonalnoci rzutowania, aby nie
narobi sobie kopotw. Tak pewno mona uzyska na przykad za pomoc sposobu z
metodami wirtualnymi, ktry zaprezentowaem w rozdziale 1.7, lub poprzez operator
typeid.
Inn moliwoci jest te uycie operatora dynamic_cast.
dynamic_cast
Przy pomocy dynamic_cast mona rzutowa wskaniki i referencje do obiektw w d
hierarchii dziedziczenia. Oznacza to, e mona zamieni odwoanie do obiektu klasy
bazowej na odwoanie do obiektu klasy pochodnej. Wyglda to np. tak:
class CFoo { /* ... */ };
class CBar : public CFoo { /* ... */ };
void Funkcja(CFoo* pFoo)
{
CBar* pBar = dynamic_cast<CBar*>(pFoo);
// ...
}
Taka zamiana nie zawsze jest moliwa, bo przecie dany wskanik (referencja)
niekoniecznie musi pokazywa na obiekt danej klasy pochodnej. Operacja jest jednak
bezpieczna, poniewa:
Poprawno rzutowania dynamic_cast jest sprawdzana w czasie dziaania programu.
Wiemy doskonale, w jaki sposb pozna rezultat tego sprawdzania. dynamic_cast
zwraca po prostu NULL (wskanik pusty, zero), jeeli rzutowanie nie mogo zosta
wykonane. Naley to zawsze skontrolowa:
if (!pBar)
{
// OK - pBar faktycznie pokazuje na obiekt klasy CBar
}
Dla skrcenia zapisu mona wykorzysta warto zwracan operatora przypisania:
if (pBar = dynamic_cast<CBar*>(pFoo))
{
// rzutowanie powiodo si
}
Znak = jest tu oczywicie zamierzony. Warunek bdzie mia bowiem warto rwn
rezultatowi rzutowania, zatem bdzie prawdziwy tylko wtedy, gdy si ono powiedzie.
Zwrcony wskanik bdzie wtedy rny od zera.
reinterpret_cast
reinterpret_cast moe suy do dowolnych konwersji midzy wskanikami, a take do
rzutowania wskanikw na typy liczbowe i odwrotnie. Wachlarz moliwoci jest wic
szeroki, niestety:
Poprawno rzutowania reinterpret_cast nie jest sprawdzana.
Zaawansowane C++ 386
atwo wic moe doj do niebezpiecznych konwersji. Ten operator powinien by
uywany tylko jako ostatnia deska ratunku - jeeli inne zawiod, a my jestemy
przekonani o wzgldnym bezpieczestwie planowanej zamiany. Wykorzystanie tego
operatora generalnie jednak powinno by bardzo rzadkie.
reintepret_cast moemy potencjalnie uy np. do uzyskania dostpu do pojedynczych
bitw w zmiennej o wikszej ich iloci:
unsigned __int32 u32Zmienna; // liczba 32-bitowa
unsigned __int8* pu8Bajty; // wskanik na liczby 8-bitowe (bajty)
// zamieniamy wskanik do 4 bajtowej zmiennej na wskanik do
// 4-elementowej tablicy bajtw
pu8Bajty = reinterpret_cast<unsigned __int8*>(&u32Zmienna);
// wywietlamy kolejne bajty zmiennej u32Zmienna
for (unsigned i = 0; i < 4; ++i)
std::cout << "Bajt nr " << i << ": " << pu8Bajty[i] << std::endl;
Wida wic, e najlepiej sprawdza si w operacjach niskopoziomowych. Tutaj monaby
oczywicie uy przesunicia bitowego, ale tablica wyglda z pewnoci przejrzyciej.
const_cast
Ostatni z nowych operatorw rzutowania ma do ograniczone zastosowanie:
const_cast suy do usuwania przydomkw const i volatile z opatrzonych nimi
wskanikw do zmiennych.
Obecno tego operatora suy chyba tylko temu, aby moliwe byo cakowite zastpienie
sposobw rzutowania znanych z C. Jego praktyczne uycie naley do sporadycznych
sytuacji.
Rzutowanie w stylu C
C++ zachowuje stare sposoby rzutowania typw. Jednym z nich jest rzutowanie
nazywane, cakiem adekwatnie, rzutowaniem w stylu C (ang. C-style cast):
(typ) wyraenie
Ta skadnia konwersji jest nadal czsto uywana, gdy jest po prostu krtsza. Naley
jednak wiedzie, e nie odrnia ona rnych sposobw rzutowania i w zalenoci od
typu i wyraenia moe si zachowywa jak static_cast, reinterpret_cast lub
const_cast.
Rzutowanie funkcyjne
Inn skadni ma rzutowanie funkcyjne (ang. function-style cast):
typ(wyraenie)
Przypomina ona wywoanie funkcji, cho oczywicie adna funkcja nie jest tu
wywoywana. Ten rodzaj rzutowania dziaa tak samo jak rzutowanie w stylu C,
aczkolwiek nie mona w nim stosowa co niektrych nazw typw. Nie mona na przykad
wykona:
int*(&fZmienna)
Zaawansowana obiektowo 387
i to z do prozaicznego powodu. Po prostu gwiazdka i nawias otwierajcy wystpujce
obok siebie zostan potraktowane jako bd skadniowy. W tej sytuacji mona sobie
ewetualnie pomc odpowiednim typedefem.
Operator typeid
typeid suy pobrania informacji o typie podanego wyraenia podczas dziaania
programu. Jest to tzw. RTTI, czyli informacja o typie czasu wykonania (ang. Run-
Time Type Information).
Przygotowanie do wykorzystania tego operatora objemuje wczenie RTTI (co dla Visual
C++ opisaem w rozdziae 1.7) oraz doczenie standardowego nagwka typeinfo:
#include <typeinfo>
Potem moemy ju stosowa typeid np. tak:
class CFoo { /* ... */ };
class CBar : public CFoo { /* ... */ };
int nZmienna;
CFoo* pFoo = new CBar;
std::cout << typeid(nZmienna).name(); // int
std::cout << typeid(pFoo).name(); // class CFoo *
std::cout << typeid(*pFoo).name(); // class CBar
Jak wida, operator ten jest leniwy i jeli tylko moe, bdzie korzysta z informacji
dostpnych w czasie kompilacji programu. Aeby wic pozna np. typ polimorficznego
obiektu, na ktry pokazujemy wskanikiem, trzeba uy derefrencji
Operatory dostpu do skadowych
Pi kolejnych operatorw suy do wybierania skadnikw klas, struktur, unii, itd. Przy
ich pomocy mona wic dosta si do zagniedonych skadowych. Nie zawsze jest to
jednak moliwe - wszystko zaley od ich widocznoci, czyli od tego, jakimi
specyfikatorami dostpu s one opatrzone (private, protected, public).
O tyche specyfikatorach mwilimy ju bardzo wiele, wic teraz przypomnijmy sobie
tylko same operatory wyuskania.
Wyuskanie z obiektu
Majc zmienn obiektow, do jej skadnikw odwoujemy si poprzez operator kropki (.),
np. tak:
struct FOO { int x; };
FOO Foo;
Foo.x = 10;
W podobny dziaa operator .*, ktry suy aczkolwiek do wyowienia skadnika poprzez
wskanik do niego:
int FOO::*p2mnSkladnik = &FOO::x;
Foo.*p2mnSkladnik = 42;
Wskaniki na skadowe s przedmiotem nastpnego podrozdziau.
Zaawansowane C++ 388
Wyuskanie ze wskanika
Gdy mamy wskanik na obiekt, wwczas zamiast kropki uywamy innego operatora
wyuskania - strzaki (->):
FOO* pFoo = new FOO;
pFoo->x = 16;
Tutaj take mamy odpowiednik, sucy do wybierania skadowych za porednictwem
wskanika na nie:
pFoo->*p2mnSkladnik += 80;
W powyszej linijce mamy dwa wskaniki, stojce po obydwu stronach operatora ->*. O
pierwszym rodzaju powiedzielimy sobie na samym pocztku programowania
obiektowego - to po prostu zwyczajny wskanik na obiekt. Drugi to natomiast wskanik
do skadowej klasy - o tym typie wskanikw pisze wicej nastpny podrozdzia.
Operator zasigu
Ten operator, nazywany te operatorem rozwikania zakresu (ang. scope resolution
operator) suy w C++ do rozrniania nazw, ktre rezyduj w rnych zakresach.
Znamy dwa podstawowe zastosowania tego operatora:
dostp do przesonitych zmiennych globalnych
dostp do skadowych klasy
Oglnie, operatora tego uywamy, aby dosta si do identyfikatora zagniedoneego
wewntrz nazwanych zakresw:
zakres_poziom1::[zakres_poziom2::[zakres_poziom3::[...]]]nazwa
Nazwy zakresw odpowiadaj m.in. strukturom, klasom i uniom. Przykadowo, FOO z
poprzedniego akapitu byo nazw zakresu - oprcz tego, rzecz jasna, take nazw
struktury. Przy pomocy operatora :: mona odnie si do jej zawartoci.
Zakresy mona te tworzy poprzez tzw. przestrzenie nazw (ang. namespaces). Jest to
bardzo dobre narzdzie, suce organizacji kodu i zapobiegajce konfliktom oznacze.
Opisuje je rozdzia Sztuka organizacji kodu.
Do tej pory cay czas korzystalimy z pewnej szczeglnej przestrzeni nazw - std.
Pamitasz doskonale, e przy niej take uywalimy operatora zakresu.
Pozostae operatory
Ostatnie trzy operatory trudno zakwalifikowa do jakiej konkretnej grupy, wic zebraem
je tutaj.
Nawiasy okrge
Nawiasy () to do oczywisty operator. W C++ suy on gwnie do:
grupowania wyrae w celu ich obliczania w pierwszej kolejnoci
deklarowania funkcji i wskanikw na nie
wywoywania funkcji
rzutowania
Brak nawiasw moe by przyczyn bdnego (innego ni przewidywane) obliczania
wyrae, a take nieprawidowej interpretacji niektrych deklaracji (np. funkcji i
wskanikw na nie). Obfite stawianie nawiasw jest szczeglnie wane w
makrodefinicjach.
Zaawansowana obiektowo 389
Z kolei nadmiar nawiasw jeszcze nikomu nie zaszkodzi :)
Operator warunkowy
Operator ?: jest nazywamy ternarnym, czyli trjargumentowym. Jako jedyny bierze
bowiem trzy dane:
warunek ? wynik_dla_prawdy : wynik_dla_faszu
Umiejtne uycie tego operatora skraca kod i pozwala unikn niepotrzebnych instrukcji
if. Co ciekawe, moe on by take uyty w deklaracjach, np. pl w klasach. Wtedy
jednak wszystkie jego operandy musz by staymi.
Przecinek
Przecinek (ang. comma) to operator o najniszym priorytecie. Oprcz tego, e oddziela
on argumenty funkcji, moe te wystpowa samodzielnie, np.:
(nX + 17, 26, rand() % 5, nY)
W takim wyraeniu operandy s obliczane od lewej do prawej, natomiast wynikiem jest
warto ostatniego wyraenia. Tutaj wic bdzie to nY.
Przecinek przydaje si, gdy chcemy wykona pewn dodatkow czynno w trakcie
wyliczania jakiej wartoci. Przykadowo, spjrzmy na tak ptl odczytujc znaki:
char chZnak;
while (chZnak = ReadChar(), chZnak != ' ')
{
// zrb co ze znakiem, ktry nie jest spacj
}
ReadChar() jest funkcj, ktra pobiera nastpny znak (np. z pliku). Sama ptla ma za
wykonywa si a do napotkania spacji. Zanim jednak mona sprawdzi, czy dany znak
jest spacj, trzeba go odczyta. Robimy to w warunku ptli, posugujc si przecinkiem.
Bez niego trzebaby najprawdopodobniej zmieni ca ptl na do, co spowodowaoby
konieczno powtrzenia kodu wywoujcego ReadChar(). Inne wyjcie to uycie ptli
nieskoczonej. C++ pozwala jednak osign ten sam efekt na kilka sposobw, spord
ktrych wybieramy ten najbardziej nam pasujcy.
Nowe znaczenia dla operatorw
Przypomnielimy sobie wszystkie operatory C++ i ich domylne znaczenia. Nam to
jednak nie wystarcza - chcemy przecie zdefiniowa dla nich cakiem nowe funkcje.
Zobaczmy zatem, jak moemy to uczyni.
Funkcje operatorowe
Pomylmy: co waciwie robi kompilator, gdy natrafi w wyraeniu na jaki operator? Czy
tylko sobie znanymi sposobami oblicza on docelow warto, czy moe jednak jest w tym
jaka zasada?
Ot tak. Dziaanie operatora definiuje pewna funkcja, zwana funkcj operatorow
(ang. operator function). Istnieje wiele takich funkcji, ktre s wbudowane w kompilator i
dziaaj na typach podstawowych. Dodawanie, odejmowanie i inne predefiniowane
dziaania na liczbach s dostpne bez adnych stara z naszej strony.
Kiedy natomiast chcemy przeciy jaki operatory, to oznacza to konieczno napisania
wasnej funkcji dla nich. Zwyczajnie, trzeba poda jej argumenty oraz warto zwracan i
Zaawansowane C++ 390
wypeni kodem. Nie ma w tym adnej magii. Za chwil zreszt przekonasz si, jak to
dziaa.
Kilka uwag wstpnych
Zobaczmy wic, jak mona zdefiniowa dodatkowe znaczenia dla operatorw w C++.
Oglna skadnia funkcji operatorowej
Przecienie operatora oznacza napisanie dla niego funkcji, odpowiedzialnej za jego nowe
dziaanie. Oto najbardziej oglna skadnia takiej funkcji:
zwracany_typ operator symbol([parametry])
{
tre_funkcji
}
Zamiast nazwy mamy tu sowo kluczowe operator, za ktrym naley poda symbol
przecianego operatora (mona go oddzieli od spacj, lecz nie jest to wymagane).
Jeeli wic chcemy np. zdefiniowia nowe znaczenie dla plusa (+), to piszemy funkcj
operator+().
Jak kada funkcja, take i ta przyjmuje pewne parametry. Ich liczba zaley cile od
tego, jaki operator chcemy przeadowa. Jeli jest to operator binarny, to si rzeczy
konieczne bd dwa parametry; dla jednoargumentowych operatorw wystarczy jeden
parametr.
Ale uwaga - parametry podane w nawiasie niekoniecznie s jedynymi, ktre funkcja
otrzymuje. Pamitasz zapewne, e metody klas maj ukryty parametr - obiekt, na rzecz
ktrego metoda zostaa wywoana, dostpny poprzez wskanik this. Ot ten parametr
jest brany pod uwag w tym przypadku. Pamitaj wic, e:
Funkcja operatorowa przyjmuje tyle argumentw, ile ma przeciany przy jej pomocy
operator. Do tych argumentw zalicza si wskanik this, jeeli funkcja operatorowa
jest metod klasy.
Od tej zasady istnieje tylko jeden wyjtek (a w zasadzie dwa). Stanowi go operatory
postinkrementacji i postdekrementacji: wprowadzono do nich dodatkowy parametr typu
int, ktry naley zignorowa. Dziki temu moliwe jest odrnienie tych operatorw od
wariantw prefiksowych.
Operatory, ktre moemy przecia
Moemy przecia bardzo wiele operatorw - zarwno takich, dla ktrych natychmiast
znajdziemy praktyczne zastosowanie, jak i tych, ktrych przecianie wydawaoby si
dziwaczne. Oto kompletna lista przecialnych operatorw:
+ - * / % & | ^ << <<
~ && || ! == != < <= > >=
+= -= *= /= %= &= |= ^= <<= >>=
++ -- = -> ->* () [] new delete ,
Tabela 18. Przecialne operatory C++
Przeadowywa moemy te i tylko te operatory. W wikszoci ksiek i kursw za chwil
nastpiaby podobna (acz znacznie krtsza) lista operatorw, ktrych przecia nie
mona. Z dowiadczenia wiem jednak, e rodzi to niewyobraaln iloc nieporozumie,
spowodowan nieprecyzyjnym okreleniem, co jest operatorem, a co nie. Dlatego te nie
podaj adnej takiej tabelki - zapamitaj po prostu, e przecia mona wycznie te
operatory, ktre wymieniem wyej.
Zaawansowana obiektowo 391
Musz jednak poda kilka wyjanie odnonie tej tabelki:
operatory: +, -, *, & mona przecia zarwno w wersji jedno-, jak i
dwuargumentowej
operatory inkrementacji (++) i dekrementacji (--) przeciamy oddzielnie dla
wersji prefiksowej i postfiksowej
przecienie new i delete powoduje take zdefiniowanie ich dziaania dla wersji
tablicowych (new[] i delete[])
operatory () i [] to nawiasy: okrge (grupowanie wyrae) i kwadratowe
(indeksowanie, wybr elementw tablicy)
operatory -> i ->* maj predefiniowane dziaanie dla wskanikw na obiekty -
jego nie moemy zmieni. Moemy natomiast zdefiniowia ich dziaanie dla
samych obiektw lub referencji do nich (domylnie takiego dziaania w ogle nie
ma)
Czego nie moemy zmieni
Przeciajc operatory moemy zdefiniowa dla nich dodatkowe znaczenie. Nie moemy
jednak:
tworzy wasnych operatorw, jak np. @, ?, === czy \
zmieni liczby argumentw, na ktrych pracuj przeciane operatory.
Przykadowo, nie stworzymy dwuargumentowego operatora ! czy te
jednoargumentowego ||
zmodyfikowa priorytetu operatora
zmieni cznoci przeadowanego operatora
Dla kadego typu C++ automatycznie generuje te pi niezbdnych operatorw, ktrych
nie musimy przecia, aby dziaay poprawnie, S to:
zwyky operator przypisania (=). Dokonuje on dosownego kopiowania obiektu
(pole po polu)
operator pobrania adresu (jednoargumentowy &). Zwraca on adres obiektu w
pamici
new dokonuje alokacji pamici dla obiektu
delete niszczy i usuwa obiekt z pamici
przecinek (,) - jego znaczenie jest takie same, jak dla typw wbudowanych
Moliwe jest aczkolwiek przecienie tych piciu symboli, aby dziaay inaczej dla naszych
klas. Nie mona jednak uniewani ich domylnej funkcjonalnoci, jak dostarcza
kompilator dla kadego typu. Mwic potocznie, nie mona ich rozdefiniowa.
Pozostae sprawy
Warto jeszcze powiedzie o pewnych naturalnych sprawach:
przynajmniej jeden argument przecianego operatora musi by innego typu ni
wbudowane. To naturalne: operatory przeciamy na rzecz wasnych typw
(klas), bo dziaania na typach podstawowych s wyaczn domen kompilatora.
Nie wtrcamy si w nie
funkcja operatorowa nie moe posiada parametrw domylnych
przecienia nie kumuluj si, tzn. jeeli na przykad przeciymy operatory +
oraz =, nie bdzie to oznaczao automatycznego zdefiniowania operatora +=.
Kade nowe znaczenie dla operatora musimy poda sami
Definiowanie przecionych wersji operatorw
Operator moemy przeciy na kilka sposobw, w zalenoci od tego, gdzie umiecimy
funkcj operatorow. Moe by ona bowiem zarwno skadnikiem (metod) klasy, na
rzecz ktrej dziaa, jak i funkcj globaln.
Zaawansowane C++ 392
Na te dwa przypadki popatrzymy sobie, definiujc operator mnoenia (dwuargumentowy
*) dla klasy CRational, znanej z poprzednich podrozdziaw. Chcemy sprawi, aby jej
obiekty mona byo mnoy przez inne liczby wymierne, np. tak:
CRational JednaPiata(1, 5), TrzyCzwarte(3, 4);
CRational Wynik = JednaPiata * TrzyCzwarte;
To bdzie spore udogodnienie, wic zobaczmy, jak mozna to zrobi.
Operator jako funkcja skadowa klasy
Wpierw sprbujmy zdefiniowa operator*() jako funkcj skadow klasy. Wiemy, e
nasz operator jest dwuargumentowy; wiemy take, e kada metoda klasy przyjmuje
jeden ukryty parametr - wskanik this. Wynika std, e funkcja operatorowa bdzie u
nas mia tylko jeden prawdziwy parametr i wygldaa na przykad tak:
CRational CRational::operator*(const CRational& Liczba) const
{
return CRational(m_nLicznik * Liczba.m_nLicznik,
m_nMianownik * Liczba.m_nMianownik);
}
To wystarczy - po tym zabiegu moemy bez problemu mnoy przez siebie zarwno dwa
obiekty klasy CRational:
CRational DwieTrzecie(2, 3), TrzySiodme(3, 7);
CRational Wynik = DwieTrzecie * TrzySiodme;
jak i jeden obiekt przez liczb cakowit:
CRational Polowa(1, 2);
CRational Calosc = Polowa * 2;
Jak to dziaa? Najlepiej przeledzi funkcjonowanie operatora, jeeli wyraenia
zawierajce go:
DwieTrzecie * TrzySiodme
Polowa * 2
zapiszemy z jawnie wywoan funkcj operatorow:
DwieTrzecie.operator*(TrzySiodme)
Polowa.operator*(2)
Wida wyranie, e pierwszy argument operatora jest przekazywany jako wskanik this.
Drugi jest natomiast normalnym parametrem funkcji operator*().
A jakim sposobem zyskalimy od razu moliwo mnoenia take przez liczby cakowite?
Myl, e to nietrudne. Zadziaaa tu po prostu niejawna konwersja, zrealizowana przy
pomocy konstruktora klasy CRational. Drugie wyraenie jest wic w rzeczywistoci
wywoaniem:
Polowa.operator*(CRational(2))
Mimochodem uzyskalimy zatem dodatkow funkcjonalno. A wszystko za pomoc
jednej funkcji operatorowej (no i jednego konstruktora).
Zaawansowana obiektowo 393
Problem przemiennoci
Nasz entuzjazm szybko moe jednak osabn. jeeli zechcemy wyprbowa
przemienno tak zdefiniowanego mnoenia. Nie bdzie przeszkd dla dwch liczb
wymiernych:
CRational Wynik = TrzySiodme * DwieTrzecie;
albo dla pary cakowita-wymierna kompilator zaprotestuje:
CRational Calosc = 2 * Polowa; // bd!
Dlaczego tak si dzieje? Ponowny rzut oka na jawne wywoanie operator*() pomoe
rozwika problem:
TrzySiodme.operator*(DwieTrzecie) // OK
2.operator*(Polowa) // ???
Wyranie wida przyczyn. Dla dwjki nie mona wywoa funkcji operator*(), bo taka
funkcja nie istnieje dla typu int - on przecie nie jest nawet klas. Nic wic dziwnego, e
uycie operatora zdefiniowanego jako metoda nie powiedzie si.
Zaraz - a co z niejawn konwersj? Dlaczego ona nie zadziaaa? Faktycznie, monaby
przypuszcza, e konstruktor konwertujcy moe zamieni 2 na obiekt klasy CRational i
uczyni wyraenie poprawnym:
CRational(2).operator*(Polowa) // OK
To jest nieprawda. Powodem jest to, i:
Niejawne konwersje nie dziaaj przy wyuskiwaniu skadnikw obiektu.
Kompilator nie rozwinie wic problematycznego wyraenia do powyszej postaci i zgosi
bd.
Operator jako zwyka funkcja globalna
Wynika z tego prosty wniosek: Houston, mamy problem :) Nie rozwiemy go na pewno,
definiujc operator*() jako funkcj skadow klasy. Trzebaby bowiem dosta si do
definicji klasy int i doda do niej odpowiedni metod. Szkoda tylko, e nie mamy
dostpu do tej definicji, co zreszt nie zaskakuje, bo int nie jest przecie adn klas.
Gdyby jednak zaoga Apollo 13 zaamywaa si po napotkaniu tak prostych problemw,
nie wrciaby na Ziemi caa i zdrowa. Nasza sytuacja nie jest a tak dramatyczna,
chocia czciowo przemienny operator mnoenia te nie jest szczytem komfortu.
Trzeba co na to poradzi.
Rozwizanie oczywicie istnieje: naley uczyni operator*() funkcj globaln:
CRational operator*(const CRational& Liczba1, const CRational& Liczba2)
{
return CRational(Liczba1.Licznik() * Liczba2.Licznik(),
Liczba1.Mianownik() * Liczba2.Mianownik());
}
Zmieni to bardzo wiele. Odtd dwa rozwaane wyraenia bd rozwijane do postaci:
operator*(TrzySiodme, DwieTrzecie) // OK
operator*(2, Polowa) // te OK!
Zaawansowane C++ 394
W tej formie oba argumenty operatora s normalnymi parametrami funkcji operator*().
Ma wic ona teraz dwa wyrane parametry, wobec ktrych moe zaj niejawna
konwersja. W tym przypadku 2 faktycznie bdzie wic interpretowane jako
CRational(2), zatem mnoenie powiedzie si bez przeszkd.
To spostrzeenie mona uoglni:
Globalna funkcja operatorowa pozwala kompilatorowi na dokonywanie niejawnych
konwersji wobec wszystkich argumentw operatora.
Jest to prosty sposb na definiowanie przemiennych dziaa na obiektach rnych typw,
midzy ktrymi istniej okrelenia konwersji.
Operator jako zaprzyjaniona funkcja globalna
Porwnajmy jeszcze tre obu wariantw funkcji operator*(): jako metody klasy
CRational i jako funkcji globalnej. Widzimy, e w pierwszym przypadku operowaa ona
bezporednio na prywatnych polach m_nLicznik i m_nMianownik. Jako funkcja globalna
musiaa z kolei posikowa si metodami dostpowymi - Licznik() i Mianownik().
Nie powinno ci to dziwi. operator*() jako zwyka funkcja globalna jest wanie -
zwyk funkcj globaln, zatem nie ma adnych specjalnych uprawnie w stosunku do
klasy CRational. Jest tak nawet pomimo faktu, e definiuje dla operacj mnoenia.
adne specjalne uprawnienia nie s potrzebne, bo funkcja doskonale radzi sobie bez
nich. Czasem jednak operator potrzebuje dostpu do niepublicznych skadowych klasy,
ktrych nie uzyska za pomoc publicznego interfejsu. W takiej sytuacji konieczne staje
si uczynienie funkcji operatorowej zaprzyjanion.
Podkrelmy jeszcze raz:
Globalna funkcja operatorowa nie musi by zaprzyjaniona z klas, na rzecz ktrej
definiuje znaczenie operatora.
Ten fakt pozwala na przecianie operatorw take dla nieswoich klas. Jak bardzo moe
to by przydatne, zobaczymy przy okazji omawiania strumieni STL z Biblioteki
Standardowej.
Sposoby przeciania operatorw
Po generalnym zapoznaniu si z przecianiem operatorw, czas na konkretne przykady.
Dowiedzmy si wic, jak przecia poszczeglne typy operatorw.
Najczciej stosowane przecienia
Najpierw poznamy takie rodzaje przecionych operatorw, ktre stosuje si najczciej.
Pomoc bdzie nam tu gwnie suy klasa CVector2D, ktr jaki czas temu
pokazaem:
class CVector2D
{
private:
float m_fX, m_fY;
public:
explicit CVector2D(float fX = 0.0f, float fY = 0.0f)
{ m_fX = fX; m_fY = fY; }
};
Zaawansowana obiektowo 395
Nie jest to przypadek. Operatory przeciamy bowiem najczeciej dla tego typu klas,
zwanych narzdziowymi. Wektory, macierze i inne przydatne obiekty matematyczne s
wanie idealnymi kandydatami na klasy z przeadowanymi operatorami.
Pokazane tu przecienia nie bd jednak tylko sztuk dla samej sztuki. Wspomniane
obiekty bd nam bowiem niezbdne z programowaniu grafiki przy uyciu DirectX. A e
przy okazji ilustruj t ciekaw technik programistyczn, jak jest przecianie
operatorw, tym lepiej dla nas :)
Spjrzmy zatem, jakie ciekawe operatory moemy przedefiniowa na potrzeby tego typu
klas.
Typowe operatory jednoargumentowe
Operatory unarne, jak sama nazwa wskazuje, przyjmuj jeden argument. Chcc dokona
ich przecienia, mamy do wyboru:
zdefiniowanie odpowiedniej metody w klasie, na rzecz ktrej dokonujemy
redefinicji:
klasa klasa::operator symbol() const;
napisanie globalnej funkcji operatorowej:
klasa operator symbol(const klasa&);
Zauwaye zapewne, e w obu wzorach podaj parametry oraz typ zwracanej
wartoci
108
. Przestrzeganie tego schematu nie jest jednak wymogiem jzyka, lecz raczej
powszechnie przyjtej konwencji dotyczcej przeciania operatorw. Mwi ona, e:
Dziaanie operatorw wobec typw zdefiniowanych przez programist powinno w miar
moliwoci pokrywa si z ich funkcjonalnoci dla typw wbudowanych.
Co to znaczy? Ot wikszo operatorw jednoargumentowych (poza
in/dekrementacj) nie modyfikuje w aden sposb przekazanych im obiektw.
Przykadowo, operator jednoargumentowego minusa - zastosowany wobec liczby zwraca
po prostu liczb przeciwn.
Chcc zachowa t konwencj, naley umieci w odpowiednich miejscach deklaracje
staoci const. Naturalnie nie trzeba tego bezwarunkowo robi - pamitajmy jednak, e
przestrzeganie szeroko przyjtych (i rozsdnych!) zwyczajw jest zawsze w interesie
programisty. Dotyczy to zarwno piszcego, jak i czytajcego i konserwujcego kod.
No, ale do tych tyrad. Pora na zastosowanie zdobytej wiedzy w praktyce. Zastanwmy
si, jakie operatory moemy logicznie przeciy dla naszej klasy CVector2D. Nie jest ich
wiele - w zasadzie tylko plus (+) oraz minus (-). Pierwszy nie powinien w ogle zmienia
obiektu wektora i zwrci go w nienaruszonym stanie, za drugi musi odda wektor o
przeciwnym zwrocie.
Sdz, e bez problemu napisaby takie funkcje. S one przecie niezwykle proste:
class CVector2D
{
// (pomijamy szczegy)
public:
// (tu te)
108
Nie dotyczy to operatorw inkrementacji i dekrementacji, ktrych omwienie znajduje si dalej.
Zaawansowane C++ 396
CVector2D operator+() const
{ return CVector2D(+m_fX, +m_fY); }
CVector2D operator-() const
{ return CVector2D(-m_fY, -m_fY); }
};
Co do drugiego operatora, to chyba nie ma adnych wtpliwoci. Natomiast
przeadowywanie plusa moe wydawa si wrcz mieszne. To jednak cakowicie
uzasadniona praktyka: jeli operator ten dziaa dla typw wbudowanych, to powinien
take funkcjononowa dla naszego wektora. Aczkolwiek tre metody operator+() to
faktycznie przykad-analogia do operator-(): rozsdniej byoby po prostu zwrci *this
(czyli kopi wektora) ni tworzy nowy obiekt.
Obie metody umieszczamy bezporednio w definicji klasy, bo s one na tyle krtkie, eby
zasugiwa na atrybut inline.
Inkrementacja i dekrementacja
To, co przed chwil powiedziaem o operatorach jednoargumentowych, nie stosuje si do
operatorw inkrementacji (++) i dekrementacji (--). cile mwic, nie stosuje si w
caoci. Mamy tu bowiem dwie odmienne kwestie.
Pierwsz z nich jest to, i oba te operatory nie s ju tak grzeczne i nie pozostawiaj
swojego argumentu w stanie nienaruszonym. Potrzebny jest im wic dostp do obiektu,
ktry zezwalaby na jego modyfikacj. Trudno oczekiwa, aby wszystkie funkcje miay do
tego prawo, zatem operator++() i operator--() powinny by co najmniej
zaprzyjanione z klas. A najlepiej, eby byy po prostu jej metodami:
klasa klasa::operator++(); // lub operator--()
Druga sprawa jest nieco innej natury. Wiemy bowiem, e inkrementacja i dekrementacja
wystpuje w dwch wersjach: przedrostkowej i przyrostkowej. Z zaprezentowanej wyej
skadni wynika jednak, e moemy przeadowa tylko jedn z nich. Czy tak?
Bynajmniej. Powysza forma jest prototypem funkcji operatorowej dla
preinkrementacji, czyli dla przedrostkowego wariantu operatora. Nie znaczy to jednak,
e wersji postfiksowej nie mona przeciy. Przeciwnie, jest to jak najbardziej moliwe
w ten oto sposb:
klasa klasa::operator++(int); // lub operator--(int)
Nie jest on zbyt elegancki i ma wszelkie znamiona triku, ale na co trzeba byo si
zdecydowa Dodatkowy argument typu int jest tu niczym innym, jak rodkiem do
rozrnienia obu typw in/dekrementacji. Nie peni on poza tym adnej roli, a ju na
pewno nie trzeba go podawa podczas stosowania postfiksowego operatora ++ (--). Jest
on nadal jednoargumentowy, a dodatkowy parametr jest tylko mao satysfakcjonujcym
wyjciem z sytuacji.
W pocztakach C++ tego nie byo, gdy po prostu niemoliwe byo przecianie
przyrostkowych operatorw inkrementacji (dekrementacji). Pniej jednak stao si to
dopuszczalne - opucimy ju jednak zason milczenia na sposb, w jaki to zrealizowano.
Tak samo jak w przypadku wszystkich operatorw zaleca si, aby zachowanie obu wersji
++ i -- byo spjne z typami podstawowymi. Jeli wic przeciamy prefiksowy
operator++() lub (i) operator--(), to w wyniku powinien on zwraca obiekt ju po
dokonaniu zaoonej operacji zwikszenia o 1.
Dla spokoju sumienia lepiej te przeciy obie wersje tych operatorw. Nie jest to
uciliwe, bo moemy korzysta z ju napisanych funkcji. Oto przykad dla CVector2D:
Zaawansowana obiektowo 397
// preinkrementacja
CVector2D CVector2D::operator++() { ++m_fX; ++m_fY; return *this; }
// postinkrementacja
CVector2D CVector2D::operator++(int)
{
CVector2D vWynik = *this;
++(*this);
return vWynik;
}
// (dekrementacja przebiega analogicznie)
Spostrzemy, e nic nie stoi na przeszkodzie, aby w postinkrementacji uy operatora
preinkrementacji:
++(*this);
Przy okazji mona dostrzec wyranie, dlaczego wariant prefiskowy jest wydajniejszy. W
odmianie przyrostkowej trzeba przecie ponie koszt stworzenia tymczasowego obiektu,
aby go potem zwrci jako rezultat.
Typowe operatory dwuargumentowe
Operatory dwuargumentowe, czyli binarne, przyjmuj po argumenty. Powiedzmy sobie
od razu, e nie musz by to operandy tych samych typw. Wobec tego nie ma czego
takiego, jak oglna skadnia prototypu funkcji operatora binarnego.
Ponownie jednak moemy mie do czynienia z dwoma drogami implementacji takiej
funkcji:
jako metody jednej z klas, na obiektach ktrej pracuje operator. Jego jawne
wywoanie wyglda wwczas tak:
operand1.operator symbol(operand2)
jako funkcji globalnej - zaprzyjanionej bd nie:
operator symbol(operand1, operand2)
Obie linijki zastpuj normalne uycie operatora w formie:
operand1 symbol operand2
O tym, ktra moliwo przeciania jest lepsza, wspominaem ju na pocztku. Przy
wyborze najwiksz rol odgrywaj ewentualne niejawne konwersje - jeeli chcemy, by
kompilator takowych dokonywa.
W bardzo uproszczonej formie mona powiedzie, e jeli jednym z argumentw ma by
typ wbudowany, to funkcja operatorowa jest dobrym kandydatem na globaln (z
przyjani bd nie, zalenie od potrzeb). W innym przypadku moemy pozosta przy
metodzie klasy - lub kierowa si innymi przesankami, jak w poniszych przykadach
Celem ujrzenia tych przykadw wrmy do naszego wektora. Jak wiemy, na wektorach
w matematyce moemy dokonywa mnstwa operacji. Nie wszystkie nas interesuj, wic
tutaj zaimplementujemy sobie tylko:
dodawanie i odejmowanie wektorw
mnoenie i dzielenie wektora przez liczb
iloczyn skalarny
Zaawansowane C++ 398
Czy bdzie to trudne? Myl, e ani troch. Zacznijmy od dodawania i odejmowania:
class CVector2D
{
// (pomijamy szczegy)
// dodawanie
friend CVector2D operator+(const CVector2D& vWektor1,
const CVector2D& vWektor2)
{
return CVector2D(vWektor1.m_fX + vWektor2.m_fX,
vWektor1.m_fY + vWektor2.m_fY);
}
// (analogicznie definiujemy odejmowanie: operator-())
};
Zastosowaem tu funkcj zaprzyjanion - przypominam przy okazji, e nie jest to
metoda klasy CVector2D, cho pewnie na to wyglda. Umieszczenie jej wewntrz bloku
klasy to po prostu zaakcentowanie faktu, e funkcja niejako naley do definicji wektora
- nie tej stricte programistycznej, ale matematycznej. Oprcz tego pozwala nam to na
zgrupowanie wszystkich funkcji zwizanych z wektorem w jednym miejscu, no i na
czerpanie zalet wydajnociowych, bo przecie operator+() jest tu funkcj inline.
Kolejny punkt programu to mnoenie i dzielenie przez liczb. Tutaj opaca si zdefiniowa
je jako metody klasy:
class CVector2D
{
// (pomijamy szczegy)
public:
// (tu te)
// mnoenie wektor * liczba
CVector2D operator*(float fLiczba) const
{ return CVector2D(m_fX * fLiczba, m_fY * fLiczba); }
// (analogicznie definiujemy dzielenie: operator/())
};
Dlaczego? Ano dlatego, e pierwszy argument ma by naszym wektorem, zatem
odpowiada nam fakt, i bdzie to this. Drugi operand deklarujemy jako liczb typu
float.
Ale chwileczk Przecie mnoenie jest przemienne! W naszej wersji operatora * liczba
moe jednak sta tylko po prawej stronie!
Ha, a nie mwiem! operator*() jako metoda jest niepoprawny - trzeba zdefiniowa go
jako funkcj globaln! Hola, nie tak szybko. Faktycznie, powysza funkcja nie wystarczy,
ale to nie znaczy, e mamy j od razu wyrzuca. Przy zastosowaniu funkcji globalnych
musielibymy przecie take napisa ich dwie sztuki:
CVector2D operator*(const CVector2D& vWektor, float fLiczba);
CVector2D operator*(float fLiczba, const CVector2D& vWektor);
Zaawansowana obiektowo 399
W kadym wic przypadku jeden operator*() nie wystarczy
109
. Musimy doda jego
kolejn wersj:
class CVector2D
{
// (pomijamy szczegy)
// mnoenie liczba * wektor
friend CVector2D operator*(float fLiczba, const CVector2D& vWektor)
{ return vWektor * fLiczba; }
};
Korzystamy w niej z uprzednio zdefiniowanej. Kwestia, czy naley poprzedni wersj
operatora take zamieni na zwyk funkcj zaprzyjanion, jest otwarta. Jeeli razi ci
niekonsekwencja (jeden wariant jako metoda, drugi jako zwyka funkcja), moesz to
zrobi.
Na koniec dokonamy trzeciej definicji operator*(). Tym razem jednak bdzie to
operator mnoenia dwch wektorw - czyli iloczynu skalarnego (ang. dot product).
Przypomnijmy, e takie dziaanie jest po prostu sum iloczynw odpowiadajcych sobie
wsprzdnych wektora. Jego wynikiem jest wic pojedyncza liczba.
Poniewa operator bdzie dziaa na dwch obiektach CVector2D, decyzja co do sposobu
jego zapisania nie ma znaczenia. Aby pozosta w zgodzie z tym ustalonym dla
operatorw dodawania i mnoenia, niech bdzie to funkcja zaprzyjaniona:
class CVector2D
{
// (pomijamy szczegy)
// iloczyn skalarny
friend float operator*(const CVector2D& vWektor1,
const CVector2D& vWektor2)
{
return vWektor1.m_fX * vWektor2.m_fX,
+ vWektor1.m_fY * vWektor2.m_fY;
}
};
Definiowanie operatorw binarnych jest wic bardzo proste, czy nie? :D
Operatory przypisania
Teraz porozmawiamy sobie o pewnym wyjtkowym operatorze. Jest on unikalny pod
wieloma wzgldami; mowa o operatorze przypisania (ang. assignment operator)
tudzie podstawienia.
Do czsto nie potrzebujemy nawet jego wyranego zdefiniowania. Kompilator dla
kadej klasy generuje bowiem taki operator, o domylnym dziaaniu. Taki automatyczny
operator dokonuje przypisania skadnik po skadniku - tak wic po jego zastosowaniu
przypisywane obiekty s sobie rwne na poziomie wartoci pl
110
. Taka sytuacja nam
czsto odpowiada - przykadowo, dla naszej klasy CVector2D bdzie to idealne
rozwizanie. Niekiedy jednak nie jest to dobre wyjcie - za chwil zobaczymy, dlaczego.
Powiedzmy jeszcze tylko, e domylny operator przypisania nie jest tworzony przez
kompilator, jeeli klasa:
109
Pomijam tu zupenie fakt, e za chwil funkcj t zdefiniujemy po raz trzeci - tym razem jako iloczyn
skalarny dwch wektorw.
110
W tym kopiowanie pole po polu wykorzystywane s aczkolwiek indywidualne operatory przypisania od klas,
ktre instancjujemy w postaci pl. Nie zawsze wic obiekty takie faktycznie s sobie doskonale rwne.
Zaawansowane C++ 400
ma skadnik bdcy sta (const typ) lub staym wskanikiem (typ* const)
posiada skadnik bdcy referencj
istnieje prywatny (private) operator przypisania:
w klasie bazowej
w klasie, ktrej obiekt jest skadnikiem naszej klasy
Nawet jeli aden z powyszych punktw nie dotyczy naszej klasy, domylne dziaanie
operatora przypisania moe nam nie odpowiada. Wtedy naley go zdefiniowa samemu
w ten oto sposb:
klasa& klasa::operator=(const klasa&);
Jest to najczstsza forma wystpowania tego operatora, umoliwiajca kontrol
przypisywania obiektw tego samego typu co macierzysta klasa. Moliwe jest aczkolwiek
przypisywanie dowolnego typu - czasami jest to przydatne.
Jest jednak co, na co musimy zwrci uwag w pierwszej kolejnoci:
Operatory przypisania (zarwno prosty, jak i te zoone) musz by zdefiniowane jako
niestatyczna funkcja skadowa klasy, na ktrej pracuj.
Wida to z zaprezentowanej deklaracji. Nie wida z niej jednak, e:
Przeciony operator przypisania nie jest dziedziczony.
Dlaczego - o tym mwiem przy okazji wprowadzania samego dziedziczenia.
OK, wystarczy tej teorii. Czas zobaczy definiowanie tego opratora w praktyce.
Wspomniaem ju, e dla klasy CVector2D w zupenoci wystarczy operator tworzony
przez kompilator. Mamy jednak inn klas, dla ktrej jest to wrcz niedopuszczalne
rozwizanie. To CIntArray, nasza tablica liczb.
Dlaczego nie moemy skorzysta dla z niej z przypisania skadnik po skadniku? Z
bardzo prostego powodu: spowoduje to przecie skopiowanie wskanikw na tablice, a
nie samych tablic.
Zauwamy, e z tego samego powodu napisalimy dla CIntArray konstruktor kopiujcy.
To nie przypadek.
Jeeli klasa musi mie konstruktor kopiujcy, to najprawdopodobniej potrzebuje take
wasnego operatora przypisania (i na odwrt).
Zajmijmy si wic napisaniem tego operatora. Aby to uczyni, pomylmy, co powinno si
sta w takim przypisaniu:
CIntArray aTablica1(7), aTablica2(8);
aTablica1 = aTablica2;
Po jego dokonaniu obie tablice musza zawiera te same elementy, lecz jednoczenie by
niezalene - modyfikacja jednej nie moe pociga za sob zmiany zawartoci drugiej.
Operator przypisania musi wic:
zniszczy tablic w obiekcie aTablica1
zaalokowa w tym obiekcie tyle pamici, aby pomieci zawarto aTablica2
skopiowa j tam
Te trzy kroki s charakterystyczne dla wikszoci implementacji operatora przypisania.
Dziel one kod funkcji operatorowej na dwie czci:
cz destruktorow, odpowiedzialn za zniszczenie zawartoci obiektu, ktry
jest celem przypisania
Zaawansowana obiektowo 401
cz konstruktorow, zajmujc si kopiowaniem
Nie mona jednak ograniczy go do prostego wywoania destruktora, a potem
konstruktora kopiujcego - choby z tego wzgldu, e tego drugiego nie da si tak po
prostu wywoa.
Dobrze, teraz to ju naprawd zaczniemy co kodowa :) Napiszemy operator przypisania
dla klasy CIntArray:
CIntArray& CIntArray::operator=(const CIntArray& aTablica)
{
// usuwamy nasz tablic
delete[] m_pnTablica;
// alokujemy tyle pamici, aby pomieci przypisywan tablic
m_uRozmiar = aTablica.m_uRozmiar;
m_pnTablica = new int [m_uRozmiar];
// kopiujemy tablic
memcpy (m_pnTablica, aTablica.m_pnTablica, m_uRozmiar * sizeof(int));
// zwracamy wynik
return *this;
}
Nie jest on chyba niespodziank - mamy tu wszystko, o czym mwilimy wczeniej. Tak
wic na pocztku zwalniamy tablic w obiekcie, bdcym celem przypisania. Pniej
alokujemy now - na tyle du, aby zmieci przypisywany obiekt. Wreszcie dokonujemy
kopiowania.
I pewnie jeszcze tylko jedna sprawa zaprzta twoj uwag: dlaczego funkcja zwraca w
wyniku *this?
Nie jest trudno odpowiedzie na to pytanie. Po prostu realizujemy tutaj konwencj znan
z typw podstawowych, mwic o rezultacie przypisania, Pozwala to te na
dokonywanie wielokrotnych przypisa, np. takich:
CIntArray aTablica1(4), aTablica2(5), aTablica3(6);
aTablica1 = aTablica2 = aTablica3;
Powyszy kod bedzie dziaa identycznie, jak dla typw podstawowych. Wszystkie tablice
stan si wic kopiami obiektu aTablica3.
Aby to osign, wystarczy trzyma si prostej zasady:
Operator przypisania powinien zwraca referencj do *this.
Wydawaoby si, e teraz wszystko jest ju absolutnie w porzdku, jeeli chodzi o
przypisywanie obiektw klasy CIntArray. Niestety, znowu zawodzi nas czujno.
Popatrzmy na taki oto kod:
CIntArray aTablica;
aTablica = aTablica; // co si stanie z tablic?
By moe przypisywanie obiektu do niego samego jest dziwne, ale jednak kompilator
dopuszcza je dla typw podstawowych, gdy jest dla nich nieszkodliwe. Nie mona tego
samego powiedzie o naszej klasie i jej operatorze przypisania.
Wywoanie funkcji operator=() spowoduje bowiem usunicie wewntrznej tablicy w
obu obiektach (bo s one przecie jednym i tym samym bytem), a nastpnie prb
Zaawansowane C++ 402
skopiowania tej usunitej tablicy do nowej! Bdziemy mogli mwi o szczciu, jeli
spowoduje to tylko bd access violation i awaryjne zakoczenie programu
Przed tak ewentualnoci musimy si wic zabezpieczy. Nie jest to trudne i ogranicza
si do prostego sprawdzenia, czy nie mamy do czynienia z przypisywaniem obiektu do
jego samego. Robimy to tak:
klasa& klasa::operator=(const klasa& obiekt)
{
if (&obiekt == this) return *this;
// (reszta instrukcji)
return *this;
}
albo tak:
klasa& klasa::operator=(const klasa& obiekt)
{
if (&obiekt != this)
{
// (reszta instrukcji)
}
return *this;
}
W instrukcji if porwnujemy wskaniki: adres przypisywanego obiektu oraz this. W ten
wyapujemy ich ewentualn identyczno i zapobiegamy katastrofie.
Operator indeksowania
Skoro jestemy ju przy naszej tablicy, warto zaj si operatorem o wybitnie
tablicowym charakterze. Mwi oczywicie o nawiasach kwadratowych [], czyli
operatorze indeksowania (ang. subscript operator).
Operator ten definiujemy zwykle w taki oto sposb:
typ_wartoci& klasa::operator[](typ_klucza);
Znowu widzimy, e jest to metoda klasy i po raz kolejny nie jest to przypadkiem:
Operator indeksowania musi by zdefiniowany jako niestatyczna metoda klasy.
To ju drugi operator, ktrego dotyczy taki wymg. Podpada pod niego jeszcze nastpna
dwjka, ktrej przecianie omwimy za chwil. Najpierw zajmijmy si operatorem
indeksowania.
Przede wszystkim chciaby pewnie wiedzie, jak on dziaa. Nie jest to trudne; jeeli
przeciymy ten operator, to wyraenie w formie:
obiekt[klucz]
zostanie przez kompilator zinterpretowane jako wywoanie w postaci:
obiekt.operator[](klucz)
Zaawansowana obiektowo 403
Do funkcji operatorowej poprzez parametr trafia wic klucz, czyli warto, jak
podajemy w nawiasach kwadratowych. Co ciekawe, nie musi to by wcale warto typu
int, ani nawet warto liczbowa - rwnie dobrze sprawdza si tu cakiem dowolny typ
danych, nawet napisy. Pozwala to tworzy klasy tzw. tablic asocjacyjnych, znanych na
przykad z jzyka PHP
111
.
Poniewa wspomniaem ju o tablicach, zajmijmy si t, ktra sami kiedy napisalimy i
cigle udoskonalamy. Nie da si ukry, e CIntArray wiele zyska na przecieniu
operatora []. Jeeli zrobimy to umiejtnie, bdzie mona uywac go tak samo, jak
czynimy to w stosunku do zwykych tablic jzyka C++.
Aby jednak to zrobi, musimy zwrci uwag na pewien szczeglny fakt. W stosunku do
typw wbudowanych operator [] jest mianowicie bardzo elastyczny: w szczeglnoci
pozwala on zarwno na odczyt, jak i modyfikacj elementw tablicy:
int aTablica[10]
aTablica[7] = 100; // zapis
std::cout << aTablica[7]; // odczyt
Wyraenie z operatorem [] moe sta zarwno po lewej, jak i po prawej stronie znaku
przypisania. T cech wypadaoby zachowa we wasnej jego wersji - znaczy to, e:
Operator indeksowania powinien w wyniku zwraca l-warto.
Gwarantuje to, e jego uycie bdzie zgodne z tym dla typw podstawowych.
Zaakcentowaem ten wymg, piszc w skadni operatora referencj jako typ zwracanej
wartoci. To wanie spowoduje podane zachowanie.
Jeeli nie moemy sobie pozwoli sobie na zwracanie l-wartoci, to powinnimy raczej
cakowicie zrezygnowa z przeadowania operatora [] i poprzesta na metodach
dostpowych - takich jak Pobierz() i Ustaw() w klasie CIntArray.
Zabierzmy si teraz do pracy: napiszemy przecion wersj operatora indeksowania dla
klasy CIntArray. Dziki temu bdziemy mogli manipulowa elementami tablicy w taki
sam sposb, jaki znamy dla normalnych tablic. To bdzie cakiem spory krok naprzd.
Osignicie tego nie jest przy tym trudne - wrcz przeciwnie, u nas bdzie niezwykle
proste:
int& CIntArray::operator[](unsigned uIndeks)
{ return m_pnTablica[uIndeks]; }
To wszystko! Zwrcenie referencji do elementu w prawidziwej, wewntrznej tablicy
pozwoli na niczym nieskrpowany dostp do jej zawartoci. Teraz moemy w wygodny
sposb odczytywa i zapisywa liczby w naszej tablicy:
CIntArray aTablica(4);
aTablica[0] = 1;
aTablica[1] = 4;
aTablica[2] = 9;
aTablica[3] = 16;
for (unsigned i = 0; i < aTablica.Rozmiar(); ++i)
std::cout << aTablica[i] << ", ";
111
Zazwyczaj lepszym rozwizaniem jest skorzystanie z mapy STL, czyli klasy std::map. Omwimy j, kiedy
przejdziemy do opisu klas pojemnikowych Biblioteki Standardowej.
Zaawansowane C++ 404
Obecnie jest ju ona funkcjonalnie identyczna z tablic typu int[]. Moemy jednak
zacz czerpa take pewne korzyci z napisania tej klasy. Skoro juz przeciamy
operator [], to zadbajmy, aby wykonywa po drodze jak poyteczn czynno - na
przykad sprawdza poprawno danego indeksu:
int& CIntArray::operator[](unsigned uIndeks)
{ return m_pnTablica[uIndeks < m_uRozmiar ? uIndeks : m_uRozmiar-1];
}
Przy takiej wersji funkcji nie grozi nam ju bd przekroczenia zakresu (ang. subscript out
of range). W razie podania nieprawidowego numeru elementu, funkcja zwrci po prostu
odwoanie do ostatniej liczby w tablicy. Nie jest to najlepsze rozwizanie, ale
przynajmniej zabezpiecza przed bdem czasu wykonania.
Znacznie lepszym wyjciem jest rzucenie wyjtku, ktry poinformuje wywoujcego o
zainstaniaym problemie. O wyjtkach porozmawiamy sobie w nastpnym rozdziale.
Operatory wyuskania
C++ pozwala na przeadowanie dwch operatorw wyuskania: -> oraz ->*. Nie jest to
czsta praktyka, a jeli nawet jest stosowana, to przecianiu podlega zwykle tylko
pierwszy z tych operatorw. Moesz wic pomin ten akapit, jeeli nie wydaje ci si
konieczna znajomo sposobu przeadowywania operatorw wyuskania.
Operator ->
Operator -> kojarzy nam si z wybieraniem skadnika poprzez wskanik do obiektu.
Wyglda to np. tak:
CFoo* pFoo = new CFoo;
pFoo->Metoda();
delete pFoo;
Jeeli jednak sprbowalimy uy tego operatora w stosunku do samego obiektu (lub
referencji do niego):
CFoo Foo;
Foo->Metoda(); // !!!
to bez wtpienia otrzymalibymy komunikat o bdzie. Domylnie nie jest bowiem
moliwe uycie operatora -> w stosunku do samych obiektw. Jest on aplikowalny tylko
do wskanikw.
Ale w C++ nawet ta elazna moe zosta nagita. Moliwe jest bowiem nadanie
operatorowi -> znaczenia i dopuszczenie do jego uywania razem ze zmiennymi
obiektowymi. Aby to uczyni, trzeba oczywicie przeciy ten operator.
Czynimy to tak oto funkcj:
jaka_klasa* klasa::operator->();
Nie wyglda ona na skomplikowan ale znowu jest to metoda klasy! Tak wic:
Operator wyuskania -> musi by niestatyczn funkcj skadow klasy.
Powiedzmy sobie teraz, jak on dziaa. Nie jest przecie wcale takie oczywiste - choby z
tego wzgldu, e z niewiadomych na razie powodw operator zadowala si zaledwie
jednym argumentem (Jest on rzecz jasna przekazywany poprzez wskanik this)
Zaawansowana obiektowo 405
A oto i odpowied. Kiedy przeciymy operator ->, wyraenie w formie:
obiekt->skadnik
zostanie zmienione na:
(obiekt.operator->())->skadnik
Mamy tu ju jawne wywoanie operator->(), ale nadal pojawia si strzaka w swej
normalnej postaci. Ot jest to konieczne; w tym kodzie -> stojcy tu przy skadniku
jest ju bowiem zwykym operatorem wyuskania ->. Zwykym - to znaczy takim, ktry
oczekuje wskanika po swojej lewej stronie - a nie obiektu, jak operator przeciony.
Wynika z tego wyraenie:
obiekt.operator->()
musi reprezentowa wskanik, aby cao dziaaa poprawnie. Dlatego te funkcja
operator->() zwraca w wyniku typ wskanikowy. Jednoczenie nie interesuje si ona
tym, co stoi po prawej stronie strzaki - to jest ju bowiem spraw tego normalnego,
wbudowanego w kompilator operatora ->.
Podsumowujc, mona powiedzie, e:
Funkcja operator->() dokonuje raczej zamiany obiektu na wskanik ni faktycznego
przedefiniowania znaczenia operatora ->.
Godne uwagi jest to, e wskanik zwracany przez t funkcje wcale nie musi by
wskanikiem na obiekt jej macierzystej klasy. Moe to by wskanik na dowoln klas,
co zreszt obrazuje skadnia funkcji.
Zastanawiasz si pewnie: A po co mi przecianie tego operatora? Moe po to, aby do
skadnikw obiektu odnosi si nie tylko kropk (.), ale i strzak (->)? Odradzam
przecianie operatora w tym celu, bo to raczej ukryje bdy w kodzie ni uatwi
programowanie.
Operator -> moemy jednak przeciy i bdzie to przydatne przy pisaniu klas tzw.
inteligentnych wskanikw.
Inteligentny wskanik (ang. smart pointer) to klasa bdca opakowaniem dla
normalnych wskanikw i zapewniajca wobec nich dodatkowe, inteligentne
zachowanie.
Rodzajw tych inteligentnych zachowa jest doprawdy mnstwo. Moe to by kontrola
odwoa do wskanika - zarwno w prostej formie zliczania, jak i zaawansowanej
komunikacji z mechanizmem zajmujcym si usuwaniem nieuywanych obiektw
(odmiecaczem, ang. garbage collector). Innym zastosowaniem moe by ochrona przed
wyciekami pamici spowodowanymi nagym opuszczeniem zakresu.
My napiszemy sobie najprostsz wersj takiego wskanika. Bdzie on przechowywa
odwoanie do obiektu CFoo, ktre przekaemy mu w konstruktorze, i zwalnia je w swoim
destruktorze. Oto kod klasy wskanika:
class CFooSmartPtr
{
private:
// opakowywany, waciwy wskanik
CFoo* m_pWskaznik;
Zaawansowane C++ 406
public:
// konstruktor i destruktor
CFooSmartPtr(CFoo* pFoo) : m_pWskaznik(pFoo) { }
~CFooSmartPtr() { if (m_pWskaznik) delete m_pWskaznik; }
//-------------------------------------------------------------
// operator dereferencji
CFoo& operator*() { return *m_pWskaznik; }
// operator wyuskania
CFoo* operator->() { return m_pWskaznik }
};
Ta klasa jest ubosz wersj std::auto_ptr z Biblioteki Standardowej. Suy ona do
bezpiecznego obchodzenia si z pamici w sytuacjach zwizanych z wyjtkami.
Omwimy j sobie w nastpnym rozdziale (wrcimy tam zreszt take i do powyszej
klasy).
Co nam daje taki wskanik? Jeeli go uyjemy, to zapobiegnie on wyciekowi pamici,
ktry moe zosta spowodowany przez nage opuszczenie zakresu (np. w wyniku wyjtku
- patrz nastpny rozdzia). Jednoczenie nie umniejszamy sobie w aden sposb wygody
kodowania - nadal moemy korzysta ze skadni, do ktrej si przyzwyczailimy:
CFooSmartPtr pFoo = new CFoo;
// wywoanie metody na dwa sposoby
pFoo->Metoda(); // naprawd: (pFoo.operator->())->Metoda()
(*pFoo).Metoda(); // naprawd: (pFoo.operator*()).Metoda()
Prosz tylko nie sdzi, e odtd powinnimy uywa tylko takich sprytnych wskanikw.
O nie, one nie s panaceum na wszystko i maj cakiem konkretne zastosowania. Nie
naley ich traktowac jako zoty rodek - szczeglnie jako rodek przeciwko
zapomnialskiemu niezwalnianiu zaalokowanej pamici.
Ciekawostka: operator ->*
Drugi z operatorw wyuskania, ->*, jest bardzo rzadko uywany. Nie dziwi wic, e
sytuacje, w ktrych jest on przeciany, s wrcz sporadyczne. Niemniej, skoro ju
mwimy o przecianiu, to moemy wspomnie take o nim.
Wpierw przydaoby si aczkolwiek, aby zna mechanizm wskanikw na skadowe klasy,
opisany w nastpnym podrozdziale.
->* jest uywany do wybierania skadnikw obiektu poprzez wskaniki do skadowych.
Podobnie jak ->, nie ma on predefiniowanego znaczenia dla zmiennych obiektowych, a
jedynie dla wskanikw na obiekty. Na tym jednak podobiestwa si kocz.
->* jest przeciany jako operator binarny dla konkretnego zestawu dwch danych,
ktre stanowi:
referencja do obiektu (argument lewostronny)
wskanik do skadowej klasy (argument prawostronny)
Nie ma te wymogu, aby funkcja operator->*() bya funkcj skadow klasy. Moe by
rwnie dobrze funkcj globaln.
Jak wic przeciy ten operator? Poniewa, jak mwiem, definiujemy go dla
konkretnego typu skadnika, posta prototypu funkcji operator->*() rni si dla
wskanikw do pl oraz do metod klasy.
Zaawansowana obiektowo 407
W pierwszym przypadku skadnia przecienia wyglda mniej wicej tak:
typ_pola& klasa::operator->*(typ_pola klasa::*);
typ_pola& operator->*(klasa&, typ_pola klasa::*);
Jest chyba do logiczne, e typ docelowego pola oraz typ zwracany przez funkcj
operatorow musi si zgadza. Do podobnie jest dla metod:
zwracany_typ klasa::operator->*(zwracany_typ (klasa::*)([parametry]));
zwracany_typ operator->*(klasa&, zwracany_typ (klasa::*)([parametry]));
Tutaj funkcja musi zwraca ten sam typ, co metoda klasy, na ktrej wskanik
przyjmujemy.
Jak wyglda przecianie w praktyce? Spjrzmy na przykad na tak oto klas:
class CFoo
{
public:
int nPole1, nPole2;
//-------------------------------------------------------------
// operator ->*
int& operator->*(int CFoo::*) { return nPole1; }
};
Po takim redefiniowaniu operatora, wszystkie wskaniki na skadowe typu int w klasie
CFoo bd prowadziy tylko i wycznie do pola nPole1.
Operator wywoania funkcji
Czas na kolejny operator, chyba jeden z bardziej interesujcych. To operator
wywoania funkcji (ang. function-call operator), czyli nawiasy okrge ().
Nawiasy maj jeszcze dwa znaczenia w C++: grupuj one wyraenia oraz pozwalaj
wykonywa rzutowanie (w stylu C lub funkcyjnym). adnego z tych pozostaych znacze
nie moemy jednak zmienia. Przecieniu moe ulec tylko operator wywoania funkcji.
Tak jest, on take moe by przeciony. O czym w tym przypadku naley pamita?
Ot:
Operator wywoania funkcji moe by zdefiniowany tylko jako niestatyczna funkcja
skadowa klasy.
Jest to ostatni rodzaj operator, ktrego dotyczy to ograniczenie. Przypominam, e
pozostaymi s: operatory przypisania, indeksowania oraz wyuskania (->).
Na tym zastrzeeniu kocz si jednak jakiegolwiek obostrzeenia nakadane na to
przecienie. operator()() (tak, dwie pary nawiasw) moe by bowiem funkcj
przyjmujc dowolne argumenty i zwracajc dowolny typ wartoci:
zwracany_typ klasa::operator()([parametry]);
To jedyny operator, ktry moe przyjmowa kad ilo argumentw. To zreszt
zrozumiae: skoro normalnie suy on do wywoywania funkcji, mogcych mie przecie
dowoln liczb parametrw, to i jego przeciona wersja nie powinna nakada
ogranicze w tym zakresie. Podobnie dzieje si, jeeli chodzi o typ zwracanej wartoci.
Zaawansowane C++ 408
Oznacza to rwnie, e moliwe jest zdefiniowanie wielu wersji przecionego operatora
(). Musz one jednak by rozrnialne w tym sam sposb, jak przeadowane funkcje.
Powinny wic posiada inn liczb, kolejno i/lub typy parametrw.
Do czego moe nam przyda si taka potga i elastyczno? Moliwoci jest bardzo wiele,
moe do nich nalee np. wybr elementu tablicy wielowymiarowej. Do ciekawszych
zastosowa naley jednak tworzenie tzw. obiektw funkcyjnych (ang. function
objects) - funktorw.
Funktory s to obiekty przypominajce zwyke funkcje, jednak rni si tym, i mog
posiada stan. Maj go, poniewa w rzeczywistoci s to klasy, ktre zawieraj jakie
publiczne pola, za skadni wywoania funkcji uzyskuj za pomoc przecienia
operatora ().
Oto prosty przykad - funktor obliczajcy redni arytmetyczn z podanych liczb i
aktualizujcy wynik z kadym kolejnym wywoaniem:
class CAverageFunctor
{
private:
// aktualny wynik
double m_fSrednia;
// ilo wywoa
unsigned m_uIloscLiczb;
public:
// konstruktor
CAverageFunctor() : m_fSrednia(0.0), m_uIloscLiczb(0) { }
//-------------------------------------------------------------
// funkcja resetujca stan funktora
void Reset() { m_fSrednia = m_uIloscLiczb = 0; }
//-------------------------------------------------------------
// operator wywoania funkcji - oblicza redni
double operator()(double fLiczba)
{
// liczymy now redni, uwzgldniajc dodan liczb
// oraz aktualizujemy zmienn przechowuj ilo liczb
// wszystko w jednym wyraeniu - za to kochamy C++ ;D
m_fSrednia = ((m_fSrednia * m_uIloscLiczb) + fLiczba)
/ m_uIloscLiczb++);
// zwracamy now redni
return m_fSrednia;
}
};
Uycie tego obiektu wyglda tak:
CAverageFunctor Srednia;
Srednia(4); // rednia z 4
Srednia(18.5); // rednia z 4 i 18.5
Srednia(-6); // rednia z 4, 18.5 i -6
Srednia(42); // rednia z 4, 18.5, -6 i 42
Srednia.Reset(); // zresetowanie funktora, warto przepada
Srednia(56); // rednia z 56
Zaawansowana obiektowo 409
Srednia(90); // rednia z 56 i 90
Srednia(4 * atan(1)); // rednia z 56, 90 i pi
std::cout << Srednia(13); // wywietlenie redniej z 56, 90, pi i 13
Naturalnie, matematycy zapaliby si za gow widzc taki algorytm obliczania redniej.
Bardzo skutecznie prowadzi on bowiem to kumulowania bdw zwizanych z
niedokadnym zapisem liczb w komputerze. Jest to jednak cakiem dobra ilustracja
koncepcji funkctora.
W Bibliotece Standardowej mamy cakiem sporo klas funktorw, z ktrymi bdziesz mg
si wkrtce zapozna.
Operatory zarzdzania pamici
Oto kolejne dwa wyjtkowe operatory: new i delete. Jak doskonale wiemy, su one do
dynamicznego tworzenia w pamici operacyjnej (a dokadniej na stercie) zmiennych,
tablic i obiektw. To moe wydawa si niemal niesamowite, ale je take moemy
przeadowa!
Wpierw jednak musz przypomnie, e praca tych operatorw nie ogranicza si w
rzeczywistoci tylko do przydzielenia pamici (new) i jej zwolnienia (delete). Jestemy
wiadomi, e moe za tym i take zainicjowanie lub sprztniecie alokowanego obszaru
pamici. Oznacza to na przykad wywoanie konstruktora (new) i destruktora (delete)
klasy, ktrej obiekt tworzymy.
Widzimy wic, e oba operatory wykonuj wicej ni jedn czynno. Zmodyfikowa
moemy jednak tylko jedn z nich:
Przecione operatory new i delete mog jedynie zmieni sposb alokowania i
zwalniania pamici. Nie mona ingerowa w inicjalizacj (wywoanie konstruktorw) i
sprztanie (przywoanie destruktorw), ktre temu towarzysz.
Zauwamy, e fakt ten niweluje dla nas rnice midzy operatorem new a new[] oraz
delete i delete[]. Na poziomie alokacji (zwalniania) pamici niczym si one bowiem nie
rni. Dlatego te dla potrzeb przeciania mwimy tylko o operatorach new i delete,
majc jednak w pamici t uwag.
Czy to, e kontrolujemy jedynie zarzdzanie pamici znaczy, e przecianie tych
operatorw nie jest interesujce? Przeciwnie - alokacja i zwalnianie pamici to s
wanie te czynnoci, ktre najbardziej nas interesuj. Napisanie wasnego algorytmu ich
wykonywania, albo chocia ledzenia tych standardowych, jest podstaw dziaania tak
zwanych menederw pamici (ang. memory managers). S to mechanizmy
zajmujce si kontrol wykorzystania pamici operacyjnej, zapobiegajce zwykle jej
wyciekom i czsto optymalizujce program.
Stworzenie dobrego menedera pamici nie jest oczywicie proste, jednak przecienie
new i delete to bardzo atwa czynno. Aby j wykona, spjrzmy na prototypy obu
funkcji - operator new() i operator delete():
void* [klasa::]operator new(size_t);
void [klasa::]operator delete(void*);
To nie pomyka: funkcje te maj cile okrelone listy parametrw oraz typy zwracanych
wartoci. W tym wzgldzie jest to wyjtek wrd wszystkich operatorw.
operator new() przyjmuje jeden parametr typu size_t - jest to ilo bajtw, jaka ma
by zaalokowana. W zamian powinien on zwrci void* - jak mona si domyla:
wskanik do przydzielonego obszaru pamici o danym rozmiarze.
Zaawansowane C++ 410
Z kolei funkcja dla operatora delete potrzebuje tylko parametru, bdcego wskanikiem.
Jest to rzecz jasna wskanik do obszaru pamici, ktry ma by zwolniony. W zamian
funkcja zwraca void, czyli nic. Oczywiste.
Mniej oczywista jest opcjonalna fraza klasa::. Owszem, sugeruje ona, e obie funkcje
mog by metodami klasy lub funkcjami globalnymi. W przeciwiestwie do pozostaych
operatorw ma to jednak znaczenie: new i delete jako metody maj bowiem inne
znaczenie ni new i delete - funkcje globalne. Mamy mianowicie moliwo lokalnego
przecienia obydwu operatorw, jak rwnie zdefiniowania ich nowych, globalnych
wersji. Omwimy sobie oba te przypadki.
Lokalne wersje operatorw
Operatory new i delete moemy przeciy w stosunku do pojedynczej klasy. W takiej
sytuacji bd one uywane do alokowania i (lub) zwalniania pamici dla obiektw
wycznie tej klasy.
Moe to si przyda np. do zapobiegania fragmentacji pamici, spowodowanej czstym
tworzeniem i zwalnianiem maych obiektw. W takim przypadku operator new moe
zarzdza wikszym kawakiem pamici i wirtualnie odcina z niego mniejsze
fragmenty dla kolejnych obiektw. delete dokonywaby wtedy tylko pozornej dealokacji
pamici.
Zobaczmy zatem, jak odbywa si przeadowanie lokalnych operatorw new i delete. Oto
prosty przykad, korzystajcy w zasadzie ze standardowych sposb przydzielania i
oddawania pamici, ale jednoczenie wypisujcy informacje o tych czynnociach:
class CFoo
{
public:
// new
void* operator new(size_t cbRozmiar)
{
// informacja na konsoli
std::cout << "Alokujemy " << cbRozmiar << " bajtow";
// alokujemy pami i zwracamy wskanik
return ::new char [cbRozmiar];
}
// delete
void operator delete(void* pWskaznik)
{
// informacja
std::cout << "Zwalniamy wskaznik " << pWskaznik;
// usuwamy pami
::delete pWskaznik;
}
};
Kiedy teraz sprbujemy stworzy dynamicznie obiekt klasy CFoo:
CFoo* pFoo = new CFoo;
to odbdzie si to z jednoczesnym powiadomieniem o tym fakcie przy pomocy strumienia
wyjcia. Analogicznie bdzie w przypadku usunicia:
delete pFoo;
Zaawansowana obiektowo 411
Nadal jednak moemy skorzysta z normalnych wersji new i delete - wystarczy
poprzedzi ich nazwy operatorem zakresu:
CFoo* pFoo = ::new CFoo;
// ...
::delete pFoo;
Tak te robimy w ciele naszych funkcji operatorowych. Mamy dziki temu pewno, e
wywoujemy standardowe operatory i nie wpadamy w puapk nieskoczonej rekurencji.
W przypadku lokalnych operatorw nie jest to bynajmniej konieczne, ale warto tak czyni
dla zaznaczenia faktu korzystania z wbudowanych ich wersji.
Globalna redefinicja
new i delete moemy te przeadowa w sposb caociowy i globalny. Zastpimy w ten
sposb wbudowane sposoby alokacji pamici dla kadego uycia tych operatorw.
Wyjtkiem bdzie tylko jawne poprzedzenie ich operatorem zakresu, ::.
Jak dokona takiego fundamentalnego przecienia? Bardzo podobnie, jak to robilimy w
trybie lokalnym. Tym razem nasze funkcje operator new() i operator delete() bda
po prostu funkcjami globalnymi:
// new
void* operator new(size_t cbRozmiar)
{
// informacja na konsoli
std::cout << "Alokujemy " << cbRozmiar << " bajtow";
// alokujemy pami i zwracamy wskanik
return ::new char [cbRozmiar];
}
// delete
void operator delete(void* pWskaznik)
{
// informacja
std::cout << "Zwalniamy wskaznik " << pWskaznik;
// usuwamy pami
::delete pWskaznik;
}
Ponownie peni one u nas wycznie funkcj monitorujc, ale to oczywicie nie jest
jedyna moliwo. Wszystko zaley od potrzeb i fantazji.
Koniecznie zwrmy jeszcze uwag na sposb, w jaki w tych przecianych funkcjach
odwoujemy si do oryginalnych operatorw new i delete. Uywamy ich w formie ::new i
::delete, aby omykowo nie uy wasnych wersji ktre przecie wanie piszemy!
Gdybymy tak nie robili, spowodowaoby to wpadnicie w niekoczcy si cig wywoa
rekurencyjnych. Pamitajmy zatem, e:
Jeli w treci przecionych, globalnych operatorw new i delete musimy skorzysta z ich
standardowej wersji, koniecznie naley uy formy ::new i ::delete.
Z domylnych wersji operatorw pamici moemy te korzysta wiadomie nawet po ich
przecieniu:
int* pnZmienna1 = new int; // przeciaona wersja
int* pnZmienna2 = ::new int; // oryginalna wersja
Zaawansowane C++ 412
Naturalnie, trzeba wtedy zdawa sobie spraw z tego przecienia i na wasne yczenie
uy operatora ::. To gwarantuje nam, e nikt inny, jak tylko kompilator bdzie
zajmowa si zarzdzaniem pamici.
Nie wpadajmy jednak w paranoj. Jeeli korzystamy z kodu, w ktrym
zaimplementowano inny sposb nadzorowania pamici, to nie naley bez wyranego
powodu z niego rezygnowa. W kocu po to kto (moe ty?) pisa w mechanizm, eby
by on wykorzystywany w praktyce, a nie z premedytacj omijany.
Cay czas mniej lub bardziej subtelnie sugeruj, e operatory new i delete naley
przecia razem. Nie jest to jednak formalny wymg jzyka C++ i jego kompilatorw.
Zwykle jednak tak wanie trzeba czyni, aby wszystko dziaao poprawnie - zwaszcza,
jeli stosujemy inny ni domylny sposb alokacji pamici.
Operatory konwersji
Na koniec przypomn jeszcze o pewnym mechanizmie, ktry w zasadzie nie zalicza si do
operatorw, ale uywa podobnej skadni i dlatego take nazywamy go operatorami.
Rzecz jasna s to operatory konwersji.
Skadnia takich operatorw to po prostu:
klasa::operator typ();
Jak doskonale pamitamy, celem funkcji tego typu jest zmiana obiektu klasy do danego
typu. Przy jej pomocy kompilator moe dokonywa niejawnych konwersji.
Innym (lecz nie zawsze stosowalnym) sposobem na osignicie podobnych efektw jest
konstruktor konwertujcy. O obu tych drogach mwilimy sobie wczeniej.
Wskazwki dla pocztkujcego przeciacza
Przecianie operatorw jest wspania moliwoci jzyka C++. Nie ma jednak adnego
przymusu stosowania jej - do powiedzie, e do tej pory wietnie radzilimy sobie bez
niej. Nie ma aczkolwiek powodu, aby j cakiem odrzuca - trzeba tylko nauczy si j
waciwie wykorzystywa. Temu wanie suy ten paragraf.
Zachowujmy sens, logik i konwencj
Jakkolwiek jzyk C++ jest znany ze swej elastycznoci, przez lata jego uytkowania
wypracowano wiele regu, dzcych midzy innymi dziaaniem operatorw. Chcc
przecia operatory dla wasnych klas, naleaoby ich w miar moliwoci przestrzega -
zwaszcza, e czsto s one zbiene ze zdrowym rozsdkiem.
Podczas przeadowania operatorw trzeba po prostu zachowa ich pierwotny sens. Jak to
zrobi?
Symbole operatorw powinny odpowiada ich znaczeniom
W pierwszej kolejnoci naley powstrzyma si od radosnej twrczoci, sprzecznej z
wszelk logik. Moe i zabawne bdzie uycie operatora == jako symbolu dodawania, ^ w
charakterze operatora mnoenia i & jako znaku odejmowania. Pomyl jednak, co w takiej
sytuacji oznacza bdzie zapis:
if (Foo ^ Bar & (Baz == Qux) == Thud)
agodnie mwic: nie jest to zbyt oczywiste, prawda? Pamitaj zatem, eby symbole
operatorw odpowiaday ich naturalnym znaczeniom, a nie tworzyy uciliwe dla
programisty rebusy.
Zaawansowana obiektowo 413
Zapewnijmy analogiczne zachowania jak dla typw wbudowanych
Wszystkie operatory posiadaj ju jakie zdefiniowane dziaanie dla typw wbudowanych.
Dla naszych klas moe ono cakiem rni si od tego pocztkowego, ale dobrze byoby,
aby przynajmniej zalenoci midzy poszczeglnymi operatorami zostay zachowane.
Co to znaczy? Zauwamy na przykad, e trzy ponisze instrukcje:
int nA;
// o te
nA = nA + 1;
nA += 1;
nA++;
dla typu int (i dla wszystkich podstawowych typw) s w przyblieniu rwnowane.
Dobrze byoby, aby dla naszych przeadowanych operatorw te tosamoci zostay
zachowane.
Podobnie jest dla typw wskanikowych:
CFoo* pFoo = new CFoo;
// instrukcje robice to samo
pFoo->Metoda();
(*pFoo).Metoda();
// ewentualnie jeszcze pFoo[0].Metoda()
delete pFoo;
Jeli tworzymy klasy inteligentnych wskanikw, naleaoby wobec tego przeciy dla
nich operatory ->, * i ewentualnie [] (a take operator bool(), aby mona je byo
stosowa w wyraeniach warunkowych).
Nie przeciajmy wszystkiego
Na koniec jeszcze jedna, oczywista uwaga: nie ma sensu przecia wszystkich
operatorw - przynajmniej do chwili, gdy nie piszemy klasy symulujcej wszystkie typy w
C++. Jeeli mimo wszystko wykonamy t niepotrzebn zwykle prac i udostpnimy
nasz piknie opakowan klas innym programistom, najprawdopodobniej zignoruj oni
te przecienia, ktre nie bd miay dla nich sensu. A jeli sami uywa bdziemy takiej
klasy, to zapewne szybko sami przekonamy si, e uporczywe uywanie operatorw nie
ma zbytniego sensu. Drog naturalnej selekcji w obu przypadkach zostan wic w uyciu
tylko te operatory, ktre s naprawd potrzebne.
Nie powinnimy jednak czeka, a ycie zweryfikuje nasze przypuszczenia, bo
przeciajc niepotrzebnie operatory, stracimy mnstwo czasu. Lepiej wic od razu
zastanowi si, co warto przeadowa, a czego nie. Kierujmy si w tym jedn, prost
zasad:
Symbol operatora powinien kojarzy si z czynnoci przez niego wykonywan.
Zastosowanie si do tej reguy likwiduje zazwyczaj wikszo niepewnoci.
***
Zakoczylimy w ten sposb poznawanie przydatnej techniki programowania, jak jest
przecianie operatorw dla naszych wasnych klas.
Zaawansowane C++ 414
W nastpnym podrozdziale, dla odmiany, zapoznamy si ze znacznie mniej przydatn
technik ;)) Chodzi o wskaniki do skadnikw klasy. Mimo tej mao zachcajcej
zapowiedzi, zapraszam do przeczytania tego podrozdziau.
Wskaniki do skadowych klasy
W ostatnim rozdziale czci pierwszej poznalimy zwyke wskaniki jzyka C: pokazujce
na zmienne oraz na funkcje. Tutaj zajmiemy si pewn nowoci, jak do wskanikw
wprowadzio programowanie obiektowe: wskanikami do skadowych (ang. pointers-
to-members).
Ten podrozdzia nie jest niezbdny do kontynuowania nauki jzyka C++. Jeeli
stwierdzisz, e jest ci na razie niepotrzebny lub za trudny, moesz go opuci. Zalecam to
szczeglnie przy pierwszym czytaniu kursu.
Podobnie jak dla normalnych wskanikw, wskaniki na skadowe take mog odnosi si
do danych (pl) oraz do kodu (metod). Omwimy sobie osobno kady z tych rodzajw
wskanikw.
Wskanik na pole klasy
Wskaniki na pola klas s obiektowym odpowiednikiem zwykych wskanikw na
zmienne, jakie doskonale znamy. Funkcjonuj one jednak nieco inaczej. Jak? O tym
traktuje niniejsza sekcja.
Wskanik do pola wewntrz obiektu
Przypomnijmy, jak wyglda zwyky wskanik - na przykad na typ int:
int nZmienna;
int* pnZmienna = &nZmienna;
Zadeklarowany tu wskanik pnZmienna zosta ustawiony na adres zmiennej nZmienna.
Wobec tego ponisza linijka:
*pnZmienna = 14;
spowoduje przypisanie liczby 14 do nZmienna. Stanie si to za porednictwem wskanika.
Wskanik na obiekt
To ju znamy. Wiemy te, e moemy tworzy take wskaniki do obiektw swoich
wasnych klas:
class CFoo
{
public:
int nSkladnik;
};
CFoo Foo;
CFoo* pFoo = &Foo;
Przy pomocy takich wskanikw moemy odnosi si do skadnikw obiektu. W tym
przypadku moemy na przykad zmodyfikowa pole nSkladnik:
Zaawansowana obiektowo 415
pFoo->nSkladnik = 76;
Sprawi to rzecz jasna, e zmieni si pole nSkladnik w obiekcie Foo - jego adres ma
bowiem wskanik pFoo. Wypisanie wartoci pola tego obiektu:
std::cout << Foo.nSkladnik;
uwiadomi wic nam, e ma ono warto 76. Ustawilimy j bowiem za porednictwem
wskanika. To te ju znamy dobrze.
Pokazujemy na skadnik obiektu
Czas wic na nowo. Pytanie brzmi: czy zwykym wskanikiem mona odnie si do
pola we wntrzu obiektu?
A owszem. Wystarczy pomyle, e wyraenie:
Foo.nSkladnik
jest l-wartoci typu int, zatem mona pobra jej adres zapisa we wskaniku typu
int*:
int* pnSkladnikFoo = &(Foo.nSkladnik);
Powiedzmy jeszcze wyranie, co tu zrobilimy. Ot pobralimy adres konkretnego pola
(nSkladnik) w konkretnym obiekcie (Foo). Jest to najzupeniej moliwe, bo przecie
obiekt reprezentuj w pamici jego pola. Skoro za moemy odnie si do obiektu jako
caoci, to moemy take pobra adres jego pl.
Jeli teraz wypiszemy wartoc pola przy pomocy tego wskanika:
std::cout << *pnSkladnikFoo;
to zobaczymy oczywicie 76, jako e nic nie zmienilimy od poprzedniego akapitu.
Musz jeszcze powiedzie, e manewr z pobraniem adresu pola w obiekcie powiedzie si
tylko wtedy, jeeli to pole jest publiczne. W innej sytuacji wyraenie Foo.nSkladnik
zostanie odrzucone przez kompilator.
Zawsze mona aczkolwiek pobiera adresy pl wewntrz klasy (np. w jej metodach) oraz
w funkcjach i klasach zaprzyjanionych. Te obszary kodu maj bowiem dostp do
wszystkich skadnikw - take niepublicznych i mog z nimi robi cokolwiek: na przykad
pobiera ich adresy w pamici.
Wskanik do pola wewntrz klasy
Kontynuujemy nasz zabaw. Teraz wemy pod lup troch inn klas, z ktr ju
mnstwo razy si spotykalimy - wektor:
struct VECTOR3 { float x, y, z; };
Formalnie jest to struktura, ale jak wiemy, w C++ rnica midzy struktur a klas jest
drobnostk i sprowadza si do domylnej widocznoci skadnikw. Dla swka struct jest
to public, wic nasze trzy pola s tu publiczne bez koniecznoci jawnego okrelania tego
faktu.
Majc klas (albo struktur - jak kto woli) z trzema polami moemy j naturalnie
instancjowa (czyli stworzy jej obiekt):
Zaawansowane C++ 416
VECTOR3 Wektor;
Nastpnie moemy te pobra adres jej pola - ktrej ze wsprzdnych:
float* pfX = &Wektor.x;
Miejsce pola w definicji klasy
Przyjrzyjmy si jednak definicji klasy. Mamy w niej trzy takie same pola, nastpujce
jedno po drugim. Pierwsze (x), drugie (y) i trzecie (z) Jeeli ci to pomoe, moesz
nawet wyobrazi sobie nasz wektor jako trjelementow tablic, w ktrej nazwalimy
poszczeglne elementy (pola). Zamiast odwoywa si do nich poprzez indeksy,
potrafimy posuy si ich nazwami (x, y, z).
Porwnanie z tablic jest jednak cakiem trafne - choby dlatego, e nasze pola s
uoone w pamici w kolejnoci wystpowania w definicji klasy. Najpierw mamy wic x,
potem y, a dalej z. Polu x moemy wic przypisa indeks 0, y - 1, a dla z indeks 2.
Sowo indeks bior tu w cudzysw, bo jest to tylko takie pojcie pomocnicze. Wiesz, e
w przypadku tablic indeksy s ostatecznie zamieniane na wskaniki w ten sposb, e do
adresu caej tablicy (czyli jej pierwszego elementu) dodawany jest indeks:
int* aTablica[5];
// te dwie linijki s rwnowane
aTablica[3] = 12;
*(aTablica + 3) = 12;
Dodawanie, jakie wystpuje w ostatnim wierwszu, nie jest dosownym dodaniem trzech
bajtw do wskanika aTablica, jest przesuniciem si o trzy elementy. Waciwie wic
kompilator zamienia to na:
aTablica + 3 * sizeof(int)
i tak oto uzyskuje adres czwartego elementu tablicy (o indeksie 3). Spjrzmy na
dodawane wyraenie:
3 * sizeof(int)
Okrela ono przesunicie (ang. offset) elementu tablicy o indeksie 3 wzgldem jej
pocztku. Znajc t warto kompilator oraz adres pierwszego elementu tablicy,
kompilator moe wyliczy pozycj w pamici dla elementu numer 3.
Dlaczego jednak o tym mwi? Ot bardzo podobna operacja zachodzi przy
odwoywaniu si do pola w obiekcie klasy (struktury). Kiedy bowiem odnosimy si
jakiego pola w ten oto sposb:
Wektor.y
to po pierwsze, kompilator zamienia to wyraenie tak, aby posugiwa si wskanikami,
bo to jest jego mow ojczyst:
(&Wektor)->y
Nastpnie stosuje on ten sam mechanizm, co dla elementw tablic. Oblicza wic adres
pola (tutaj y) wedug schematu:
&Wektor + offset_pola_y
Zaawansowana obiektowo 417
W tym przypadku sprawa nie jest aczkolwiek taka prosta, bo definicja klasy moe
zawiera pola wielu rnych typw o rnych rozmiarach. Offset nie bdzie wic mg by
wyliczany tak, jak to si dzieje dla elementu tablicy. On musi by znany ju wczeniej
Skd?
Z definicji klasy! Okrelajc nasz klas w ten sposb:
struct VECTOR3 { float x, y, z; };
zdefiniowalimy nie tylko jej skadniki, ale te kolejno pl w pamici. Oczywicie nie
musimy podawa dokadnych liczb, precyzujcych pooenie np. pola z wzgldem obiektu
klasy VECTOR3. Tym zajmie si ju sam kompilator: przeanalizuje ca definicj i dla
kadego pola wyliczy sobie oraz zapisze gdzie odpowiednie przesunicie.
I t wanie liczb nazywamy wskanikiem na pole klasy:
Wskanik na pole klasy jest okreleniem miejsca w pamici, jakie zajmuje pole
danej klasy, wzgldem pocztku obiektu w pamici.
W przeciwniestwie do zwykego wskanika nie jest to wic liczba bezwzgldna. Nie
mwi nam, e tu-i-tu znajduje si takie-a-takie pole. Ona tylko informuje, o ile bajtw
naley si przesun, poczynajc od adresu obiektu, a znale w pamici konkretne pole
w tym obiekcie.
Moe jeszcze lepiej zrozumiesz to na przykadzie kodu. Jeeli stworzymy sobie obiekt
(statycznie, dynamicznie - niewane) - na przykad obiekt naszego wektora:
VECTOR3* pWektor = new VECTOR3;
i pobierzemy adres jego pola - na przykad adres pola y w tym obiekcie:
int* pnY = &pWektor->y;
to rnica wartoci obu wskanikw (adresw) - na obiekt i na jego pole:
pnY - pWektor
bedzie niczym innym, jak wanie offsetem tego pola, czyli jego miejscem w definicji
klasy! To jest ten rodzaj wskanikw C++, jakim si chcemy tutaj zaj.
Pobieranie wskanika
Zauwamy, e offset pola jest wartoci globaln dla caej klasy. Kady bowiem obiekt
ma tak samo rozmieszczone w pamici pola. Nie jest tak, e wrd kilku obiektw naszej
klasy VECTOR3 jeden ma pola uoone w kolejnoci x, y, z, drugi - y, z, x, trzeci - z, y, x,
itp. O nie, tak nie jest: wszystkie pola s poukadane dokadnie w takiej kolejnoci,
jak ustalilimy w definicji klasy, a ich umiejscowienie jest dla kadego obiektu
identyczne.
Uzyskanie offsetu danego pola, czyli wskanika na pole klasy, moe wic odbywa si bez
koniecznoci posiadania obiektu. Wystarczy tylko poda, o jak klas i o jakie pole nam
chodzi, np.:
&VECTOR3::y
Powysze wyraenie zwrci nam wskanik na pole y w klasie VECTOR3. Powtarzam
jeszcze raz (aby dobrze to zrozumia), i bdzie to ilo bajtw, o jak naley si
Zaawansowane C++ 418
przesun poczynajc od adresu jakiego obiektu klasy VECTOR3, aby natrafi na pole y
tego obiektu. Jeeli jest to dla ciebie zbyt trudne, to moesz mysle o tym wskaniku
jako o indeksie pola y w klasie VECTOR3.
Deklaracja wskanika na pole klasy
No dobrze, pobranie wskanika to jedno, ale jego zapisanie i wykorzystanie to zupenie
co innego. Najpierw wic dowiedzmy si, jak mona zachowa warto uzyskan
wyraeniem &VECTOR3::y do pniejszego wykorzystania.
By moe domylasz si, e bdzie potrzebowali specjalnej zmiennej typu
wskanikowego - czyli wskanika na pole klasy. Aby go zadeklarowa, musimy
przypomnie sobie, czym charakteryzuj si wskaniki w C++.
Nie jest to trudne. Kady wskanik ma swj typ: w przypadku wskanikw na zmienne
by to po prostu typ docelowej zmiennej. Dla wskanikw na funkcje sprawa bya bardziej
skomplikowana, niemniej te miay one swoje typy.
Podobnie jest ze wskanikami na skadowe klasy. Kady z nich ma przypisan klas, na
ktre skadniki pokazuje - dotyczy to zarwno odniesie do pl, ktrymi zajmujemy si
teraz, jak i do metod, ktre poznamy za chwil.
Oprcz tego wskanik na pole klasy musi te zna typ docelowego pola, czyli wiedzie,
jaki rodzaj danych jest w nim przechowywany.
Czy wiemy to wszystko? Tak. Wiemy, e nasz klas jest VECTOR3. Pamitamy te, e jej
wszystkie pola zadeklarowalimy jako float. Korzystajc z tej informacji, moemy
zadeklarowa wskanik na pola typu float w klasie VECTOR3:
float VECTOR3::*p2mfWspolrzedna;
Huh, co za zakrcona deklaracja Gdzie tu jest w ogle nazwa tej zmiennej?
Spokojnie, nie jest to a takie straszne - to tylko tak wyglda :) Nasz wskanik nazywa
si oczywicie p2mfWspolrzedna
112
, za niezbyt przyjazna forma deklaracji stanie si
janiejsza, jeeli popatrzymy na jej ogln skadni:
typ klasa::*wskanik;
Co to jest? Ot jest to deklaracja wskanika, pokazujcego na pola podanego typu,
znajdujce si we wntrzu okrelonej klasy. Nic prostrzego, prawda? ;-)
Teraz, kiedy mamy ju zmienn odpowiedniego typu wskanikowego, moemy przypisa
jej wzgldny adres pola y w klasie VECTOR3:
p2mfWspolrzedna = &VECTOR3::y;
Pamitajmy, e w ten sposb nie pokazujemy na konkretn wsprzdn Y (pole y) w
konkretnym wektorze (obiekcie VECTOR3), lecz na miejsce pola w definicji klasy.
Pojedynczo taki wskanik nie jest wic uyteczny, bo jego wartoc nabiera znaczenia
dopiero w momencie zastosowania jej dla konkretnego obiektu. Jak to zrobi -
zobaczymy w nastpnym akapicie.
Zwrmy jeszcze uwage, e y nie jest jedynym polem typu float w klasie VECTOR3. Z
rwnym powodzeniem moemy pokazywa naszym wskanikiem take na pozostae:
p2mfWspolrzedna = &VECTOR3::x;
112
p2mf to skrt od pointer-to-member float.
Zaawansowana obiektowo 419
p2mfWspolrzedna = &VECTOR3::z;
Warunkiem jest jednak, aby pole byo publiczne. W przeciwnym wypadku wyraenie
klasa::pole byloby nielegalne (poza klas) i nie monaby zastosowa wobec niego
operatora &.
Uycie wskanika
Wskanik na pole klasy jest adresem wzgldnym, offsetem. Aby skorzysta z niego
praktycznie, musimy posiada jaki obiekt; kompilator bdzie dziki temu wiedzia, gdzie
si dany obiekt zaczyna w pamici. Posiadajc dodatkowo offset pola w definicj klasy,
bdziemy mogli odwoywa si do tego pola w tym konkretnym obiekcie.
A zatem do dziea. Stwrzmy sobie obiekt naszej klasy:
VECTOR3 Wektor;
Potem zadeklarujmy wskanik na i ustawmy go na jedno z trzech pl klasy VECTOR3:
float VECTOR3::*p2mfPole = &VECTOR3::x;
Teraz przy pomocy tego wskanika moemy odwoac si do tego pola w naszym obiekcie.
Jak? O tak:
Wektor.*p2mfPole = 12; // wpisanie liczby do pola obiektu Wektor,
// na ktre pokazuje wskanik p2mfPole
Caa zabawa polega tu na tym, e p2mfPole moe pokazywa na dowolne z trzech pl
klasy VECTOR3 - x, y lub z. Przy pomocy wskanika moemy jednak do kadego z nich
odwoywa si w ten sam sposb.
Co nam to daje? Mniej wicej to samo, co w przypadku zwykych wskanikw. Wskanik
na pole klasy moemy przekaza i wykorzysta gdzie indziej. W tym przypadku
potrzebujemy aczkolwiek jeszcze jednej danej: obiektu naszej klasy, w kontekcie
ktrego uyjemy wskanika.
Moe czas na jaki konkretny przykad. Wyobramy sobie funkcj, ktra zeruje jedn
wsprzdn tablicy wektorw. Teraz moemy j napisa:
void WyzerujWspolrzedna(VECTOR3 aTablica[], unsigned uRozmiar,
float VECTOR3::*p2mfWspolrzedna)
{
for (unsigned i = 0; i < uRozmiar; ++i)
aTablica[i].*p2mfWspolrzedna = 0;
}
W zalenoci od tego, jak j wywoamy:
VECTOR3 aWektory[50];
WyzerujWspolrzedna (aWektory, 50, &VECTOR3::x);
WyzerujWspolrzedna (aWektory, 50, &VECTOR3::y);
WyzerujWspolrzedna (aWektory, 50, &VECTOR3::z);
spowoduje ona wyzerowanie rnych wsprzdnych wektorw w podanej tablicy.
Wskanik na pole klasy moemy te wykorzysta, gdy na samym obiekcie operujemy
take przy pomocy wskanika (tym razem zwykego, na obiekt). Stosujemy wtedy
aczkolwiek inn skadni:
Zaawansowane C++ 420
// deklaracja i inicjalizacja obu wskanikw - na obiekt i pole klasy
VECTOR3* pWektor = new VECTOR3;
float VECTOR3::p2mfPole = &VECTOR3::z;
// zapisanie wartoci do pola z obiektu *pWektor przy pomocy wskanikw
pWektor->*p2mfPole = 42;
Jak wida, w kontekcie wskanikw na skadowe operatory .* i ->* s dokadnymi
odpowiednikami operatorw wyuskania . i ->. Tych drugim uywamy jednak wtedy, gdy
odwoujemy si do skadnikw obiektu poprzez ich nazwy, natomiast tych pierwszych -
jeli posugujemy si wskanikami do skadowych.
Operator ->*, podobnie jak ->, moe by przeciony. Z kolei .*, tak samo jak . - nie.
Wskanik na metod klasy
Normalne wskaniki mog te pokazywa na kod, czyli funkcje. Obiektowym
odpowiednikiem tego faktu s wskaniki do metod klasy. Zajmiemy si nimi w tej sekcji.
Wskanik do statycznej metody klasy
Zwyczajny wskanik do funkcji globalnej deklarujemy np. tak:
int (*pfnFunkcja)(float);
Przypominam, e aby odczyta deklaracj funkcji pasujcych do tego wskanika,
wystarczy usun gwiazdk oraz nawiasy otaczajce jego nazw. Tutaj wic moemy do
wskanika pfnFunkcja przypisa adresy wszystkich funkcji globalnych, ktre przyjmuj
jeden parametr typu float i zwracaj liczb typu int:
int Foo(float) { /* ... */ }
// ...
pfnFunkcja = Foo; // albo pfnFunkcja = &Foo;
Jednak nie tylko funkcje globalne mog by wskazywane przez takie wskaniki.
Wskaniki do zwykych funkcji potrafi te pokazywa na statyczne metody klas.
Nietrudno to wyjani. Takie metody to tak naprawd funkcje globalne o nieco
zmienionym zasigu i notacji wywoania. Najwaniejsze, e nie posiadaj one ukrytego
parametru - wskanika this - poniewa ich wywoanie nie wymaga obecnoci adnego
obiektu klasy. Nie korzystaj one wic z konwencji wywoania thiscall (waciwej
metodom niestatycznym), a zatem moemy zadeklarowa zwyke wskaniki, ktre bd
na pokazywa.
Warunkiem jest jednak to, aby metoda statyczna bya zadeklarowana jako public. W
przeciwnym razie wyraenie nazwa_klasy::nazwa_metody nie bdzie legalne.
Podobne uwagi mona poczyni dla statycznych pl, na ktre mona pokazywa przy
pomocy zwykych wskanikw na zmienne.
Zaawansowana obiektowo 421
Wskanik do niestatycznej metody klasy
A jak jest z metodami niestatycznymi? Czy na nie te moemy pokazywa zwykymi
wskanikami?
Niestety nie. Fakt ten moe si wyda zaskakujcy, ale mona go wyjani nawet na
kilka sposobw.
Po pierwsze: wspomniaem ju, e metody niestatyczne korzystaj ze specjalnej
konwencji thiscall. Oprcz normalnych parametrw musza one bowiem dosta obiekt,
ktry w ich wntrzu bdzie reprezentowany przez wskanik this. C++ nie pozwala na
zadeklarowanie funkcji uywajcych konwencji thiscall - nie bardzo wiadomo, jak taka
deklaracja miaaby wyglda
113
.
Po drugie: metody niestatyczne potrzebuj wskanika this. Gdyby dopuci do sytuacji,
w ktrej wskaniki na funkcje mog pokazywa na metody, wwczas trzebaby byo
zapewni jako dostarczenie tego wskanika this (czyli obiektu, na rzecz ktrego
metoda jest wywoywana). Jak? Poprzez dodatkowy parametr? Wtedy mielibymy
koszmarn nieciso skadni: deklaracje wskanikw do funkcji nie zgadzayby si z
prototypami pasujcych do nich metod.
Nawet jeli nie bardzo zrozumiae te argumenty, musisz przyj, e na niestatyczne
metody klasy nie pokazujemy zwykymi wskanikami do funkcji. Zamiast tego
wykorzystujemy drugi rodzaj wskanikw na skadowe klasy.
Wykorzystanie wskanikw na metody
Mam tu na myli wskaniki na metody klas.
Wskanik do metody w klasie (ang. pointer-to-member function) okrela miejsce
deklaracji tej metody w definicji klasy.
Wida tu analogie ze wskanikami do pl klasy. Tutaj take okrelamy umiejscowienie
danej metody wzgldem
No wanie - wzgldem czego?! W przypadku pl moglimy jeszcze mwi, e wskanik
jest okreleniem przesunicia (offsetu), ktry pozwala znale pole danego obiektu, gdy
mamy adres pocztku tego obiektu. Ale przecie metody nie podlegaj tym zasadom.
Dla wszystkich obiektw mamy przecie jeden zestaw metod. Jak wic mona
mwi o tym, e wskaniki na nie dziaaj w ten sam sposb?
Ekhm, tego raczej nie powiedziaem. Wskaniki te mog dziaa ten sam sposb, czyli
by adresami wzgldnymi. Mog one take by adresami bezwzgldnymi (w sumie -
dlaczego nie? Przecie metody to te funkcje), a nawet indeksami jakiej wewntrznej
tablicy czy jeszcze dziwniejszymi liczbami z gatunku identyfikatorw-uchwytw. Tak
naprawd nie powinno nas to interesowa, gdy jest to wewntrzna sprawa
kompilatora. Dla nas wskaniki te pokazuj po prostu na jak metod wewntrz danej
klasy. Jak to robi - to ju nie nasze zmartwienie.
Deklaracja wskanika
Spjrzmy lepiej na jaki przykad. Wemy tak oto klas:
class CNumber
{
private:
113
Zauwamy, e deklaracja metody wyjta z klasy i umieszczona poza ni automatycznie stanie si funkcj
globaln. Nie trzeba dokonywa adnych zmian w jej prototypie, polegajcych np. na usuniciu sowa
thiscall. Takiego sowa kluczowego po prostu nie ma: C++ odrnia metody od zwykych funkcji wycznie
po miejscu ich zadeklarowania.
Zaawansowane C++ 422
float m_fLiczba;
public:
// konstruktor
CNumber(float m_fLiczba = 0.0f) : m_fLiczba(fLiczba) { }
//-------------------------------------------------------------
// kilka metod
float Dodaj(float x) { return (m_fLiczba += x); }
float Odejmij(float x) { return (m_fLiczba -= x); }
float Pomnoz(float x) { return (m_fLiczba *= x); }
float Podziel(float x) { return (m_fLiczba /= x); }
};
Nie jest ona moe zbyt mdra - nie ma przecionych operatorw i w ogle wykonuje
do dziwn czynno enkapsulacji typu podstawowego - ale dla naszych celw bdzie
wystarczajca. Zwrmy uwag na jej cztery metody: wszystkie bior argument typu
float i tak liczb zwracaj. Jeeli chcielibymy zadeklarowa wskanik, mogcy
pokazywa na te metody, to robimy to w ten sposb
114
:
float (CNumber::*p2mfnMetoda)(float);
Wskanik p2mfnMetoda moe pokazywa na kad z tych czterech metod, tj.:
float CNumber::Dodaj(float x);
float CNumber::Odejmij(float x);
float CNumber::Pomnoz(float x);
float CNumber::Podziel(float x);
Mona std cakiem atwo wywnioskowa ogln skadni deklaracji takiego wskanika. A
wic, dla metody klasy o nagwku:
zwracany_typ nazwa_klasy::nazwa_metody([parametry])
deklaracja odpowiadajcego jej wskanika wyglda tak:
zwracany_typ (nazwa_klasy::*nazwa_wskanika)([parametry]);
Deklaracja wskanika na metod klasy wyglda tak, jak nagwek tej metody, w ktrym
fraza nazwa_klasy::nazwa_metody zostaa zastpiona przez sekwencj
(nazwa_klasy::*nazwa_wskanika). Na kocu deklaracji stawiamy oczywicie rednik.
Sposb jest wic bardzo podobny jak przy zwykych wskanikach na funkcje. Ponownie
te istotne staj si nawiasy. Gdybymy bowiem je opucili w deklaracji p2mfnMetoda,
otrzymalibymy:
float CNumber::*p2mfnMetoda(float);
co zostanie zinterpretowane jako:
float CNumber::* p2mfnMetoda(float);
114
p2mfn to skrt od pointer-to-member function.
Zaawansowana obiektowo 423
czyli funkcja biorca jeden argument float i zwracajca wskanik do pl typu float w
klasie CNumber. Zatem znowu - zamiast wskanika na funkcj otrzymujemy funkcj
zwracajc wskanik.
Dla wskanikw na metody klas nie ma problemu z umieszczenia sowa kluczowego
konwencji wywoania, bo wszystkie metody klas uywaj domylnej i jedynie susznej w
ich przypadku konwencji thiscall. Nie ma moliwoci jej zmiany (mam nadziej, e jest
oczywiste, dlaczego).
Pobranie wskanika na metod klasy
Kiedy mamy ju zadeklarowany waciwy wskanik, powimy go z ktr z metod klasy
CNumber. Robimy to w prosty i raczej przewidywalny sposb:
p2mfnMetoda = &CNumber::Dodaj;
Podobnie jak dla zwykych funkcji, take i tutaj operator & nie jest niezbdny:
p2mfnMetoda = CNumber::Odejmij;
Znowu te stosuje si tu zasada o publicznoci skadowych. Jeeli sprbujemy pobra
wskanik na metod prywatn lub chronion, to kompilator oczywicie zaprotestuje.
Uycie wskanika
Czas wreszcie na akcj. Zobaczmy, jak mona wywoa metod pokazywan przez
wskanik:
CNumber Liczba = 42;
std::cout << (Liczba.*p2mfnMetoda)(2);
Potrzebujemy naturalnie jakiego obiektu klasy CNumber, aby na jego rzecz wywoa
metod. Tworzymy go wic; dalej znowu korzystamy z operatora .*, wywoujc przy
jego pomocy metod klasy CNumber dla naszego obiektu - przekazujemy jej jednoczenie
parametr 2. Poniewa po naszej zabawie z przypisywaniem p2mfnMetoda pokazywa na
metod Odejmij(), na ekranie zobaczylibymy:
40
Zwracam jeszcze uwag na nawiasy w wywoaniu metody. Tutaj s one konieczne (w
przeciwiestwie do zwykych wskanikw na funkcje) - bez nich kompilator uzna linijk
za bdn.
Domylasz si, e jeli posiadalibymy tylko wskanik na obiekt, to do wywoania jego
metody posuylibymy si operatorem ->*. Identycznie jak przy wskanikach na pola
klasy.
Ciekawostka: wskanik do metody obiektu
Zatrzymajmy si na chwilk Jeeli przebrne przed ten rozdzia od pocztku a dotd,
to szczerze ci gratuluj. Wskaniki na skadowe nie s bynajmniej atw czci jzyka -
choby dlatego, e operuj do dziwnymi koncepcjami (miejsce w definicji klasy). Co
gorsza, czytajc o nich jako trudno od razu wpa na sensowne zastosowanie tego
mechanizmu.
Wiem, e podobne odczucia mogy ci towarzyszy przy lekturze opisw wielu innych
elementw jzyka. Pniej jednak nieczsto widziae zastosowania omawianych
wczeniej rzeczy w dalszej czciu kursu, a pewnie sam znalajdowae niektre z nich po
odpoczynku od lektury i duszym zastanowieniu.
Zaawansowane C++ 424
Tutaj musz ci nieco zmartwi. Wskaniki na skadowe klasy s w praktyce bardzo
rzadko uywane, bo w zasadzie trudno znale dla nich jakie uyteczne zastosowanie.
To chyba najdobitniejszy przykad jzykowego wodotrysku - na szczcie C++ nie
posiada zbyt wiele takich nadmiarowych udziwnie.
Sprbujemy jednak znale dla nich jakie zastosowanie Okazuje si, e jest to
moliwe. Wskanikw tych moemy bowiem uy do symulowania innego rodzaju
wskanikw - nieobecnych niestety w C++, ale za to bardzo przydatnych.
Jakie to wskaniki? Spjrz na ponisz tabel. Grupuje ona wszystkie znane (i
nieznane ;D) w programowaniu strukturalnym i obiektowym rodzaje wskanikw, wraz z
ich nazwami w C++:
rodzaj
wskanika
obiektowe
na skadowe niestatyczne cel
wskanika
strukturalne
na skadowe
statyczne
w klasach w obiektach
dane wskaniki do zmiennych
wskaniki do pl
klasy
wskaniki do
zmiennych
kod wskaniki do funkcji
wskaniki do
metod klasy
BRAK
Tabela 19. Rne rodzaje wskanikw
Wynika z niej, e znamy ju wszystkie rodzaje wskanikw, jakie posiada w swoim
arsenale C++. A co z tymi brakujcymi?
Czym one s? Ot s to takie wskaniki, ktre potrafi pokazywa na konkretn
metod w konkretnym obiekcie. Podobnie jak wskaniki do pl obiektu, s one
samodzielne. Ich uycie nie wymaga wic adnych dodatkowych informacji: dokonujc
zwyczajnej dereferencji takiego wskanika, wywoywalibymy okrelon metod w
odniesieniu do okrelonego obiektu. Zupenie tak, jak dla zwykych wskanikw do
funkcji - tyle tylko, e tutaj nie wywoujemy funkcji globaln, lecz metod obiektu.
No dobrze, nie mamy tego rodzaju wskanikw Ale co z tego? Na pewno s one rwnie
uyteczne, jak te co poznalimy niedawno! Ot wrcz przeciwnie! Tego rodzaju
wskaniki s niezwykle przydatne! Pozwalaj one bowiem na implementacj funkcji
zwrotnych (ang. callback functions) z zachowaniem penej obiektowoci programu.
C to s - te funkcje callback? S to takie funkcje, ktrych adresy przekazujemy komu,
aby ten kto mg je dla nas wywoa w odpowiednim momencie. Ten odpowiedni
moment to na przykad zajcie jakiego zdarzenia, na ktre oczekujemy (wcinicie
klawisza, wybicie pnocy na zegarze, itp.) albo chociaby wystpienie bdu. W kadej
tego typu sytuacji nasz program moe by o tym natychmiast poinformowany. Bez
funkcji zwrotnych musiaby zwykle dokonywa mozolnego odpytywania ktosia, aby
dowiedzie si, czy dana okoliczno wystpia. To mao efektywne rozwizanie.
Funkcje callback s lepsze. Jednak w C++ tylko funkcje globalne lub statyczne metody
klas mog by takimi funkcjami. Powd jest prosty: jedynie na takie metody moemy
pokazywa samodzielnymi wskanikami.
A to jest zupenie niezadowolajce w programowaniu obiektowym. Zmusza to przecie do
pisania kodu poza klasami programu. W dodatku trzeba jako zapewni sensown
komunikacj midzy tym kodem-outsiderem, a obiektow reszt programu. W sumie
mamy mnstwo kopotw.
Wymylono rzecz jasna pewien sposb na obejcie tego problemu, polegajcy na
wykorzystaniu metod wirtualnych, dziedziczenia i polimorfizmu. Nie jest to jednak idealne
rozwizanie - przynajmniej nie w C++.
Zaawansowana obiektowo 425
Powiedziaem jednak, e nasze wieo poznane wskaniki mog pomc w poradzeniu
sobie z tym problemem. Zobaczmy jak to zrobi.
Bardzo, ale to bardzo odradzam czytanie tych dwch punktw przy pierwszym kontakcie
z tekstem (to zreszt dotyczy prawie wszystkich Ciekawostek). Sprawa jest wprawdzie
bardzo ciekawa i niezwykle przydatna, lecz jej zawio moe ci szybko odstrczy od
wskanikw klasowych - albo nawet od programowania obiektowego, co by byo znacznie
gorsz katastrof.
Wskanik na metod obiektu konkretnej klasy
Najpierw zajmijmy si prostszym przypadkiem. Znajdmy sposb na symulacj
wskanika, za porednictwem ktrego monaby wywoywa metod:
o okrelonej sygnaturze (nagwku)
na rzecz okrelonego obiektu
nalecego do okrelonej klasy
Dosy duo tych okrele Najlepiej bdzie, jeli popatrzysz na dziaanie tego
wskanika. Przypomnij sobie klas CNumber; stwrzmy obiekt tej klasy:
CNumber Liczba;
Teraz wyobramy sobie, e w jzyku C++ pojawia si moliwo zadeklarowania
wskanikw, o jakie nam chodzi. Niech p2ofnMetoda bdzie tym podanym
wskanikiem
115
. Wwczas mona z nim zrobi co takiego:
// przypisanie wskanikowi "adresu metody" Dodaj w obiekcie Liczba
p2ofnMetoda = Liczba.Dodaj;
// wywoanie metody Dodaj() dla obiektu Liczba
(*p2ofnMetoda)(10);
Jak wida, dokonujemy tu zwykej dereferencji - zupenie tak, jak w przypadku
wskanikw na funkcje globalne. Tym sposobem wywoujemy jednak metod klasy dla
konkretnego obiektu. Ostatnia linijka jest wic rwnowana tej:
Liczba.Dodaj(10);
Zamiast wywoania obiekt.metoda() mamy wic (*wskanik_do_metody_obiektu)(). I
o to nam chodzi.
Wrmy teraz do rzeczywistoci. Niestety C++, nie posiada wskanikw na metody
obiektw, lecz chcemy przynajmniej czciowo uzupeni ten brak. Jak to zrobi?
Przyjrzyjmy si temu, co chcemy osign. Chcemy mianowicie, aby nasz wskanik
zastpowa wywoanie:
obiekt.metoda([parametry])
w ten sposb:
(*wskanik)([parametry])
Wskanik musi wic zawiera informacje zarwno o obiekcie, ktrego dotyczy metoda,
jak i samej metodzie. Jeden wskanik? Nie - dwa:
pierwszy to wskanik na obiekt, na rzecz ktrego metoda bdzie wywoywana
115
p2ofn to skrt od pointer to object-function.
Zaawansowane C++ 426
drugi to wskanik na metod klasy, ktra ma by wywoywana
Chcc stworzy nasz wskanik, musimy wic poczy te dwie dane. Zrbmy to! Najpierw
zdefiniujmy sobie jak klas, na ktrej metody bdziemy pokazywa:
class CFoo
{
public:
void Metoda(int nParam)
{ std::cout << "Wywolano z " << nParam; }
};
Dalej - dodajmy obiekt, ktry bdzie bra udzia w wywoaniu:
CFoo Foo;
Przypomnijmy wreszcie, e chcemy zrobi taki wskanik, ktrego uycie zastapi nam
wywoanie:
Foo.Metoda();
Potrzebujemy do tego wspomnianych dwch rodzajw wskanikw:
wskanika na obiekty klasy CFoo
wskanika na metody klasy CFoo biorce int i niezwracajce wartoci
Poczymy oba te wskaniki w jedn struktur, dodajc przy okazji pomocnicze funkcje -
jak konstruktor oraz operator():
struct METHODPOINTER
{
// rzeczone oba wskaniki
CFoo* pObject; // wskanik na obiekt
void (CFoo::*p2mfnMethod)(int); // wskanik na metod
//-------------------------------------------------------------------
// konstruktor
METHODPOINTER(CFoo* pObj, void (CFoo::*p2mfn)(int))
: pObject(pObj), p2mfnMethod(p2mfn) { }
// operator wywoania funkcji
void operator() (int nParam)
{ (pObject->*p2mfnMethod(nParam); }
};
Teraz moemy ju pokaza takim wskanikiem na metod naszego obiektu. Podajemy po
prostu zarwno wskanik na obiekt, jak i na metod klasy:
METHODPOINTER p2ofnMetoda(&Foo, &CFoo::Metoda);
To wprawdzie pewna niedogodno (nie moemy poda po prostu Foo.Metoda, lecz
musimy pamita nazw klasy), ale i tak jest to cakiem dobre rozwizanie. Nasz
metod moemy bowiem wywoa w najprostszy moliwy sposb:
p2ofnMetoda (69); // to samo co Foo.Liczba (69);
To wanie chcielimy osigna.
Zaawansowana obiektowo 427
Jest to aczkolwiek rozwizanie dla szczeglnego przypadku. A jak wyglda to w
przypadku oglnym? Mniej wicej w ten sposb:
struct WSKANIK
{
// wskaniki
klasa* pObject;
zwracany_typ (klasa::*p2mfnMethod)([parametry_formalne]);
//-------------------------------------------------------------------
// konstruktor
WSKANIK(klasa* pObj,
zwracany_typ (klasa::*p2mfn)([parametry_formalne]))
: pObject(pObj), p2mfnMethod(p2mfn) { }
// operator wywoania funkcji
zwracany_typ operator() ([parametry_formalne])
{ [return] (pObject->*p2mfnMethod([parametry_aktualne]); }
};
Niestety, preprocesor na niewiele nam si przyda w tym przypadku. Tego rodzaju
struktury musiaby wpisywa do kodu samodzielnie.
Wskanik na metod obiektu dowolnej klasy
Nasz callback wydaje si dziaa (bo i dziaa), ale jego przydatno jest niestety
niewielka. Wskanik potrafi bowiem pokazywa tylko na metod w konkretnej klasie,
natomiast do zastosowa praktycznych (jak informowanie o zdarzeniach czy bdach)
powinien on umie wskaza na zgodn ustalonym prototypem metod obiektu w
dowolnej klasie.
Tak wic niezalenie od tego, czy nasz obiekt byby klasy CFoo, CVector2D,
CEllipticTable czy CBrokenWindow, jeli tylko klasa ta posiada metod o okrelonej
sygnaturze, to powinno da si na ni wskaza w konkretnym obiekcie. Dopiero wtedy
dostaniemy do rki wartociowy mechanizm.
Ten mechanizm ma nazw: closure. Trudno to przetumaczy na polski (dosownie jest to
przymknicie, domknicie, itp.), wic bdziemy posugiwa si dotychczasow nazw
wskanik na metod obiektu. Czasami aczkolwiek spotyka si te nazw delegat
(ang. delegate).
Czy mona go osign w C++? Owszem. Wymaga to jednak do daleko idcego
kroku: ot musimy sobie zdefiniowa uniwersaln klas bazow. Z takiej klasy bd
dziedziczy wszystkie inne klasy, ktrych obiekty i ich metody maj by celami
tworzonych wskanikw. Taka klasa moe by bardzo prosta, nawet pusta:
class IObject { };
Mona do niej doda wirtualny destruktor czy inne wsplne dla wszystkich klas skadowe,
jednak to nie jest tutaj wane. Grunt, eby taka klasa bya obecna.
Teraz sprecyzujmy problem. Zamy, e mamy kilka innych klas, zawierajcych metody
o waciwej dla nas sygnaturze:
class CFoo : public IObject
{
public:
float Funkcja(int x) { return x * 0.75f; }
};
Zaawansowane C++ 428
class CBar : public IObject
{
public:
float Funkcja(int x) { return x * 1.42f; }
};
Zauwamy z IObject. Czego chcemy? Ot poszukujemy sposobu na
zaimplementowanie wskanika, ktry bdzie pokazywa na metod Funkcja() zarwno w
obiektach klasy CFoo, jak i CBar. Nawet wicej - chcemy takiego wskanika, ktry pokae
nam na dowoln metod biorc int i zwracaj float w dowolnym obiekcie
dowolnej klasy w naszym programie. Mwiem ju, e w praktyce ta dowolna klasa
musi dziedziczy po IObject.
C wic zrobi? Moe znowu signiemy po dwa wskaniki - jeden na obiekt, a drugi na
metod klasy? Punkt dla ciebie. Faktycznie, tak wanie zrobimy. Posta naszego
wskanika nie rni si wic zbytnio od tej z poprzedniego punktu:
struct METHODPOINTER
{
// rzeczone oba wskaniki
IObject* pObject; // wskanik na obiekt
float (IObject::*p2mfnMethod)(int); // wskanik na metod
//-------------------------------------------------------------------
// konstruktor
METHODPOINTER(IObject* pObj, float (IObject::*p2mfn)(int))
: pObject(pObj), p2mfnMethod(p2mfn) { }
// operator wywoania funkcji
float operator() (int x)
{ return (pObject->*p2mfnMethod(x); }
};
Chwileczk Deklarujemy tutaj wskanik na metody klasy IObject, biorce int i
zwracajce float Ale przecie IObject nie ma takich metod - ba, u nas nie ma nawet
adnych metod! Takim wskanikiem nie pokaemy wic na adn metod!
Bingo, kolejny punkt za uwan lektur :) Rzeczywicie, taki wskanik wydaje si
bezuyteczny. Pamitajmy jednak, e w sumie chcemy pokazywa na metod obiektu,
a nie na metod klasy. Za nasze obiekty bd pochodzi od klasy IObject, bo ich
wasne klasy po IObject dziedzicz. W sumie wic wskanikiem na metod klasy
bazowej bdziemy pokazywa na metod klasy pochodnej. To jest poprawne - za chwil
wyjani bliej, dlaczego.
Najpierw sprbujmy uy naszego wskanika. Stwrzmy wic obiekt ktrej z klas:
CBar* pBar = new CBar;
i ustawmy nasz wskanik na metod Funkcja() w tym obiekcie - tak, jak to robilimy
dotd:
METHODPOINTER p2ofnMetoda(pBar, &CBar::Funkcja);
I jak? Mamy przykr niespodziank. Kady szanujcy si kompilator C++ najpewniej
odrzuci t linijk, widzc niezgodno typw. Jak niezgodno?
Zaawansowana obiektowo 429
Pierwszy parametr jest absolutnie w porzdku. To znana i lubiana konwersja wskanika
na obiekt klasy pochodnej (CBar*) do wskanika na obiekt klasy bazowej (IObject*).
Brak zastrzee nikogo nie dziwi - przecie na tym opiera si cay polimorfizm.
To drugi parametr sprawia problem. Kompilator nie zezwala na zamian typu:
float (CBar::*)(int)
na typ:
float (IObject::*)(int)
Innymi sowy, nie pozwala na konwersj wskanik na metod klasy pochodnej do
wskanika na metod klasy bazowej. Jest to uzasadnione: wskanik na metod (oglnie:
na skadow) moe by bowiem poprawny w klasie pochodnej, natomiast nie zawsze
bdzie poprawny w klasie bazowej. Obiekt klasy bazowej moe by przecie mniejszy, nie
zawiera pewnych elementw, wprowadzonych w modszym pokoleniu. W takim wypadku
wskanik bdzie strzela w prni, co skoczy si bdem ochrony pamici
116
.
Tak mogoby by, jednak u nas tak nie bdzie. Naszego wskanika na metod uyjemy
przecie tylko i wyacznie do wywoania metody obiektu pBar. Klasa obiektu oraz klasa
wskanika w tym przypadku zgadzaj si, s identyczne - to CBar. Nie ma adnego
ryzyka.
Kompilator bynajmniej o tym nie wie i nie naley go wcale za to wini. Musimy sobie po
prostu pomc rzutowaniem:
METHODPOINTER p2ofnMetoda(pBar,
static_cast<float (IObject::*)(int)>
(&CBar::Funkcja));
Wiem, e wyglda to okropnie, ale przecie nic nie stoi na przeszkodzie, aby uly sobie
odpowiednim makrem.
Zreszt, liczy si efekt. Teraz moemy wywoa metod pBar->Funkcja() w ten prosty
sposb:
p2ofnMetoda (42); // to samo co pBar->Funkcja (42);
Jest te zupenie moliwe, aby pokaza naszym wskanikiem na analogiczn metod w
obiekcie klasy CFoo:
CFoo Foo;
p2ofnMetoda.pObject = &Foo;
p2ofnMetoda.p2mfnMethod = static_cast<float (IObject::*)(int)>
(&CFoo::Funkcja));
p2ofnMetoda (14); // to samo co Foo.Funkcja (14)
Zmieniajc ustawienie wskanika musimy jednak pamita, by:
Klasy docelowego obiektu oraz docelowej metody musz by identyczne. Inaczej
ryzykujemy bad ochrony pamici.
116
Konwersja w drug stron (ze wskanika na skadow klasy bazowej do wskanika na skadow klasy
pochodnej) jest z kolei zawsze moliwa. Jest tak dlatego, e klasa pochodna nie moe usun adnego
skadnika klasy bazowej, lecz co najwyej rozszerzy ich zbir. Wskanik bdzie wic zawsze poprawny.
Zaawansowane C++ 430
Zaprezentowane rozwizanie moe nie jest szczeglnie eleganckie, ale wystarczajce. Nie
zmienia to jednak faktu, e wbudowana obsuga wskanikw na metody obiektw w C++
byaby wielce podana.
Nieco lepsz implementacj wskanikw tego rodzaju, korzystajc m.in. z szablonw,
moesz znale w moim artykule Wskanik na metod obiektu.
***
Czy masz ju do? :) Myl, e tak. Wskaniki na skadowe klas (czy te obiektw) to
nie jest najatwiejsza cz OOPu w C++ - miem twierdzi, e wrcz przeciwnie. Mamy
j ju jednak za sob.
Jeeli aczkolwiek chciaby si dowiedzie na ten temat nieco wicej (take o zwykych
wskanikach na funkcje), to polecam wietn witryn The Function Pointer Tutorials.
W ten sposb poznalimy te ca ofert narzdzi jzyka C++ w zakresie programowania
obiektowego. Moemy sobie pogratulowa.
Podsumowanie
Ten dugi rozdzia by powicony kilku specyficznym dla C++ zagadnieniom
programowania obiektowego. Zdecydowana wikszo z nich ma na celu poprawienie
wygody, czasem efektywnoci i naturalnoci kodowania.
C wic zdylimy omwi?
Na pocztek poznalimy zagadnienie przyjani midzy klasami a funkcjami i innymi
klasami. Zobaczye, e jest to prosty sposb na zezwolenie pewnym cile okrelonym
fragmentom kodu na dostp do niepublicznych skadowych jakiej klasy.
Dalej przyjrzelimy si bliej konstruktorom klas. Poznalimy ich listy inicjalizacyjne, rol
w kopiowaniu obiektw oraz niejawnych konwersjach midzy typami.
Nastpnie dowiedzielimy si (prawie) wszystkiego na temat bardzo przydatnego
udogodnienia programistycznego: przeciania operatorw. Przy okazji powtrzylimy
sobie wiadomoci na temat wszystkich operatorw jzyka C++.
Wreszcie, odwaniejsi spord czytelnikw zapoznali si take ze specyficznym rodzajem
wskanikw: wskanikami na skadniki klasy.
Nastpny rozdzia bdzie natomiast powicony niezwykle istotnemu mechanizmowi
wyjtkw.
Pytania i zadania
By moe zaprezentowane w tym rozdziale techniki su tylko wygodzie programisty, ale
nie zwalnia to kodera z ich dokadnej znajomoci. Odpowiedz wic na powysze pytania i
wykonaj wiczenia.
Pytania
1. Jakie specjalne uprawnienia ma przyjaciel klasy? Co moe by takim
przyjacielem?
2. W jaki sposb deklarujemy zaprzyjanion funkcj?
3. Co oznacza deklaracja przyjani z klas?
4. Jak mona sprawi, aby dwie klasy przyjaniy si z wzajemnoci?
5. Co to jest konstruktor domylny? Jakie s korzyci klasy z jego posiadania?
Zaawansowana obiektowo 431
6. Czym jest inicjalizacja? Kiedy i jak przebiega?
7. Do czego suy lista inicjalizacyjna konstruktora?
8. Kiedy konieczny jest konstruktor kopiujcy?
9. W jaki sposb moemy definiowa niejawne konwersje?
10. Co powoduje sowo kluczowe explicit w deklaracji konstruktora?
11. Kiedy konstruktor konwertujcy jest jednoczenie domylnym?
12. Wymie podstawowe cechy operatorw w jzyku programowania.
13. Jakie rodzaje operatorw posiada C++?
14. Na czym polega przecienie operatora?
15. Jaki status mog posiada funkcje operatorowe? Czym si one rni?
16. Jak mona skorzysta z niejawnych konwersji, piszc przecione wersje
operatorw binarnych?
17. Ktre operatory mog by przeciane wycznie jako niestatyczne metody klas?
18. Kiedy konieczne jest zdefiniowanie wasnego operatora przypisania?
19. Ile argumentw ma operator wywoania funkcji?
20. O czym naley pamita, przeciajc operatory?
21. O czym informuje wskanik do skadowej klasy?
22. Jakim wskanikiem pokazujemy na pole w obiekcie, a jakim na pole w klasie?
23. Czy zwykym wskanikiem do funkcji moemy pokaza na metod obiektu?
wiczenia
1. Zdefiniuj dwie klasy, ktre bd ze sob wzajemnie zaprzyjanione.
2. Przejrzyj definicje klas z poprzednich rozdziaw i popatrz na ich konstruktory. W
ktrych przypadkach monaby uy w nich list inicjalizacyjnych?
3. Do klas CRational i CComplex dodaj operatory niejawnych konwersji na typ bool.
Co dziki temu zyskae?
4. (Trudniejsze) Wzboga wspomniane klasy take o operatory dodawania,
odejmowania i dzielenia (tylko CRational) oraz o odpowiadajce im operatory
zoonego przypisania i in/dekrementacji.
5. Napisz funktor obliczajcy najwiksz z podawanych mu liczb typu float. Niech
stosuje on ten sam interfejs i sposb dziaania, co klasa CAverageFunctor.
3
WYJTKI
Dowiadczenie - to nazwa, jak nadajemy
naszym bdom.
Oscar Wilde
Programici nie s nieomylni. O tym wiedz wszyscy, a najlepiej oni sami. W kocu to
gwnie do nich naley codzienna walka z wikszymi i mniejszymi bdami, wkradajcymi
si do kodu rdowego. Dobrze, jeli s to tylko usterki skadniowe w rodzaju braku
potrzebnego rednika albo domykajcego nawiasu. Wtedy sam kompilator daje o nich
zna.
Nieco gorzej jest, gdy mamy do czynienia z bdami objawiajcymi si dopiero podczas
dziaania programu. Moe to spowodowa nawet produkowanie nieprawidowych wynikw
przez nasz aplikacj (bdy logiczne).
Wszystkie tego rodzaju sytuacj maj jdna cech wspln. Mona bowiem (i naley) im
zapobiega: moliwe i podane jest takie poprawienie kodu, aby bdy tego typu nie
pojawiay si. Aplikacja bdzie wtedy dziaaa poprawnie
Ale czy na pewno? Czy twrca aplikacji moe przewidzie wszystkie sytuacje, w jakich
znajdzie si jego program? Nawet jeli jego kod jest cakowicie poprawny i wolny od
bdw, to czy gwarantuje to jego poprawne dziaanie za kadym razem?
Gdyby odpowied na chocia jedno z tych pyta brzmiaa Tak, to programici pewnie
rwaliby sobie z gw o poow mniej wosw ni obecnie. Niestety, nikt o zdrowym
rozsdku nie moe obieca, e jego kod bdzie zawsze dziaa zgodnie z oczekiwaniami.
Naturalnie, jeeli jest on napisany dobrze, to w wikszoci przypadkw tak wanie
bdzie. Od kadej reguy zawsze jednak mog wystpi wyjtki
W tym rozdziale bdziemy mwi wanie o takich wyjtkach - albo raczej o sytuacjach
wyjtkowych. Poznamy moliwoci C++ w zakresie obsugi takich niecodziennych
zdarze i oglne metody radzenia sobie z nimi.
Mechanizm wyjtkw w C++
Czym waciwie jest taka sytuacja wyjtkowa, ktra moe narobi tyle zamieszania?
Ot:
Sytuacja wyjtkowa (ang. exceptional state) ma miejsce wtedy, gdy warunki
zewntrzne uniemoliwiaj danemu fragmentowi kodu poprawne wykonanie. w
fragment nie jest winny zaistnienia sytuacji wyjtkowej.
Oglnie sytuacj wyjtkow mona nazwa kady bd wystpujcy podczas dziaania
programu, ktry nie jest spowodowany przez bdy w jego kodzie. To co w rodzaju
przykrej niespodzianki: nieprawidoowych danych, nieprzewidzianego braku zasobw, i
tak dalej. Takie przypadki mog zdarzy si w kadym programie, nawet napisanym
pozornie bezbdnie i dziaajcym doskonale w zwykych warunkach. Sytuacje
wyjtkowe, jak sama ich nazwa wskazuje, zdarzaj si bowiem tylko w warunkach
wyjtkowych
Zaawansowane C++ 434
Tradycyjne metody obsugi bdw
Wystpieniu sytuacji wyjtkowej zwykle nie mona zapobiec - a przynajmniej nie moe
tego zrobi ten kawaek kodu, w ktrym ona faktycznie wystpuje. Jego rol powinno by
zatem poinformowanie o zainstniaym zdarzeniu kodu, ktry stoi wyej w strukturze
programu. Kod wyszego poziomu moe wtedy podj jakie sensowne akcje, a jeli nie
jest to moliwe - w ostatecznoci zakoczy dziaanie programu.
Dziaania wykonywane w reakcji na bdy s do specyficzne dla kadego programu.
Obejmowac mog na przykad zapisanie informacji o zdarzeniu w specjalnym dzienniku,
pokazanie komunikatu dla uytkownika czy te jeszcze inne czynnoci. Tym
zagadnieniem nie bedziemy si wic zajmowa.
Zobaczmy raczej, jakimi sposobami moe odbywa si powiadamianie o bdach. Tutaj
istnieje kilka potencjalnym rozwiza - niektre s lepsze, inne nieco gorsze Oto te
najczciej wykorzystywane.
Dopuszczalne sposoby
Do cakiem dobrych metod informowania o niespodziewanych sytuacjach naley
zwracanie jakiej specjalnej wartoci - indykatora. Wywoujcy dan funkcj moe wtedy
sprawdzi, czy bd wystpi, kontrolujc rezultaty zwrcone przez podprogram.
Zwracanie nietypowego wyniku
Najprostsz drog poinformowania o bdzie jest zwrcenie pewnej specjalnej wartoci,
ktra w normalnych warunkach nie ma prawa wystpi. Aby to zilustrowa, zamy
przez chwil, e mamy napisa funkcj obliczajc pierwiastek kwadratowy z podanej
liczby. Wiedzc to, ochoczo zabieramy si do pracy, produkujc np. taki oto kod:
float Pierwiastek(float x)
{
// staa okreljca dokadno
static const float EPSILON = 0.0001f;
/* liczymy pierwiastek kwadratowy metod Newtona */
// wybieramy punkt pocztkowy (poow wartoci)
float fWynik = x / 2;
// wykonujemy tyle iteracji, aby otrzyma rozsdne przyblienie
while (abs(x - fWynik * fWynik) > EPSILON)
fWynik = (fWynik + x / fWynik) / 2;
// zwracamy wynik
return fWynik;
}
Funkcja ta wykorzystuje iteracyjn metod Newtona do obliczania pierwiastka, ale to nie
jest dla nas zbyt wane, bowiem dotyczy zwykej sytuacji. My natomiast mwimy o
sytuacjach niezwykych. Co ni bdzie dla naszej funkcji?
Na pewno bdzie to podanie jej liczby ujemnej. Dopki pozostajemy na gruncie prostej
matematyki, jest to dla nas bdna warto - nie mona wycign pierwiastka
kwadratowego z liczby mniejszej od zera.
Nie mona jednak wykluczy, e nasza funkcja otrzyma kiedy liczb ujemn. Bdzie to
bd, sytuacja wyjtkowa - i trzeba bdzie na ni zareagowa. cile mwic, trzeba
bdzie poinformowa o niej wywoujcego funkcj.
Wyjtki 435
Specjalny rezultat
Jak mona to zrobi? Prostym sposobem jest zwrcenie specjalnej wartoci. Niech
bdzie to warto, ktra w normalnych warunkach nie ma prawa by zwrcona. W tym
przypadku powinna to by taka liczba, ktrej prawidowe zwrcenie przez Pierwiastek()
nie powinno mie miejsca.
Jaka to liczba? Oczywicie - dowolna liczba ujemna. Powiedzmy, e np. -1:
if (x < 0) return -1;
Po dodaniu tego sprawdzenia funkcja bdzie ju odporna na sytuacje z nieprawidowym
argumentem. Wywoujcy j bdzie musia natomiast sprawdza, czy rezultat funkcji nie
jest przypadkiem informacj o bdzie - np. w ten sposb:
float fLiczba;
float fPierwiastek;
if ((fPierwiastek = Pierwiastek(fLiczba)) < 0)
std::cout << "Nieprawidlowa liczba";
else
std::cout << "Pierwiastek z " << fLiczba << " to " << fPierwiastek;
Jak wida, przy wykorzystaniu wartoci zwracanej operatora przypisania nie jest to
szczeglnie uciliwe.
Wady tego rozwizania
Takie rozwizanie ma jednak kilka mankamentw. Pierwsz wida ju tutaj: nie wyglda
ono szczeglnie estetycznie od strony wywoujcego. Druga kwestia jest powaniejsza.
Jest ni problem doboru wartoci specjalnej, sygnalizujcej bd. Zwracam uwag, e nie
ma ona prawa pojawienia si w jakiejkolwiek poprawnej sytuacji - musi ona
jednoznacznie identyfikowa bd, a nie przydatny rezultat.
W przypadku funkcji Pierwiastek() byo to proste, gdy potencjalnych wartoci jest
mnstwo: moemy przecie wykorzysta wszystkie liczby ujemne - poprawnym wynikiem
funkcji jest bowiem tylko liczba dodatnia. Nie zawsze jednak musi tak by - czas na
kolejny przykad matematyczny, tym razem z logarytmem o dowolnej podstawie:
float LogA(float a, float x) { return log(x) / log(a); }
Tutaj take moliwe jest podanie nieprawidowych argumentw: wystarczy, eby cho
jeden z nich by ujemny lub aby podstawa logarytmu (a) bya rwna jeden. Nie warto
polega na reakcji funkcji bibliotecznej log() w razie zaistnienia takiej sytuacji; lepiej
samemu co na to poradzi.
No wanie - ale co? Moemy oczywicie skontrolowa poprawno argumentw funkcji:
if (a < 0 || a == 1.0f || x < 0)
/* bd, ale jak o nim powiedzie?... */
ale nie bardzo wiadomo, jak specjaln warto naleaoby zwrci. W zakresie typu
float nie ma bowiem adnej wolnej liczby, poniewa poprawny wynik logarytmu moe
by kad liczb rzeczywist.
Ostatecznie mona zwrci zero, ktry to wynik zachodzi normalnie tylko dla x rwnego
1. Wwczas jednak sprawdzanie potencjalnego bdu byoby bardzo niewygodne:
// sprawdzamy, czy rezultat jest rwny zero, a argument rny od jeden;
// jeeli tak, to bd
if (((fWynik = LogA(fPodstawa, fLiczba)) == 0.0f) && fLiczba != 1.0f)
std::cout << "Zly argument funkcji";
Zaawansowane C++ 436
else
std::cout << "Logarytm o podst. " << fPodstawa << " z " << fLiczba
<< " wynosi " << fWynik;
To chyba przesdza fakt, i czenie informacji o bdzie z waciwym wynikiem nie jest
dobrym pomysem.
Oddzielenie rezultatu od informacji o bdzie
Obie te dane trzeba od siebie odseparowa. Funkcja powinna zatem zwraca dwie
wartoci: jedn waciw oraz drug, informujc o powodzeniu lub niepowodzeniu
operacji.
Ma to rozliczne zalety - midzy innymi:
pozwala przekaza wicej danych na temat charakteru bdu
upraszcza kontrol poprawnoci wykonania funkcji
umoliwia swobod zmian w kodzie i ewentualne rozszerzenie funkcjonalnoci
Wydaje si jednak, e jest do powany problem: jak funkcja miaaby zwraca dwie
wartoci? C, chyba brak ci pomysowoci - istnieje bowiem kilka drg zrealizowania
tego mechanizmu.
Wykorzystanie wskanikw
Nasza funkcja, oprcz normalnych argumentw, moe przyjmowa jeden wskanik. Za
jego porednictwem przekazana zostanie dodatkowa warto. Moe to by informacja o
bdzie, ale czciej (i wygodniej) umieszcza si tam waciwy rezultat funkcji.
Jak to wyglda? Oto przykad. Funkcja StrToUInt() dokonuje zamiany liczby naturalnej
zapisanej jako cig znakw (np. "21433") na typ unsigned:
#include <cmath>
bool StrToUInt(const std::string& strLiczba, unsigned* puWynik)
{
// sprawdzamy, czy podany napis w ogle zawiera znaki
if (strLiczba.empty()) return false;
/* dokonujemy konwersji */
// zmienna na wynik
unsigned uWynik = 0;
// przelatujemy po kolejnych znakach, sprawdzajc czy s to cyfry
for (unsigned i = 0; i < strLiczba.length(); ++i)
if (strLiczba[i] > '0' && strLiczba[i] < '9')
{
// OK - cyfra; mnoymy aktualny wynik przez 10
// i dodajemy t cyfr
uWynik *= 10;
uWynik += strLiczba[i] - '0';
}
else
// jeeli znak nie jest cyfr, to koczymy niepowodzeniem
return false;
// w przypadku sukcesu przepisujemy wynik i zwracamy true
*puWynik = uWynik;
return true;
}
Wyjtki 437
Nie jest ona moe najszybsza, jako e wykorzystuje najprostszy, naturalny algorytm
konwersji. Nam jednak chodzi o co innego: o sposb, w jaki funkcja zwraca rezultat i
informacj o ewentualnym bdzie.
Jak mona zauway, typem zwracanym przez funkcj jest bool. Nie jest to wic
zasadniczy wynik, lecz tylko znacznik powodzenia lub niepowodzenia dziaa. Zasadniczy
rezultat to kwestia ostatniego parametru funkcji: naley tam przekaza wskanik na
zmienn, ktra otrzyma wynikow liczb.
Brzmi to moe nieco skomplikowanie, ale w praktyce korzystanie z tak napisanej funkcji
jest bardzo proste:
std::string strLiczba;
unsigned uLiczba;
if (StrToUInt(strLiczba, &uLiczba))
std::cout << strLiczba << " razy dwa == " << uLiczba * 2;
else
std::cout << strLiczba << " - nieprawidlowa liczba";
Moesz si spiera: Ale przecie tutaj mamy wybitnego kandydata na poczenie
rezultatu z informacj o bdzie! Wystarczy zmieni zwracany typ na int - wtedy
wszystkie wartoci ujemne mogyby informowa o bdzie!
Chyba jednak sam widzisz, jak to rozwizanie byoby nacigane. Nie do, e uylibymy
nieadekwatnego typu danych (ktry ma mniejszy zakres interesujcych nas liczb
dodatnich ni unsigned), to jeszcze ograniczylibymy moliwo przyszej rozbudowy
funkcji. Zamy na przykad, e na bazie StrToUInt() chcesz napisa funkcj
StrToInt():
bool StrToInt(const std::string& strLiczba, int* pnWynik);
Nie jest to trudne, jeeli wykorzystujemy zaprezentowan tu technik informacji o
bdach. Gdybymy jednak poprzestali na czeniu rezultatu z informacj o bedzie,
wwczas byoby to problemem. Oto stracilibymy przecie ca ujemn powk typu
int, bo ona teraz take musiaaby by przeznaczona na poprawne wartoci.
Dla wprawy w oglnym programowaniu moesz napisa funkcj StrToInt(). Jest to
raczej proste: wystarczy doda sprawdzanie znaku minus na pocztku liczby i nieco
zmodyfikowa ptl for.
Wida wic, e mimo pozornego zwikszenia poziomu komplikacji, ten sposb
informowania o bedach jest lepszy. Nic dziwnego, e stosuj go zarwno funkcje
Windows API, jak i interfejsu DirectX.
Uycie struktury
Dla nieobytych ze wskanikami (mam nadziej, e do nich nie naleysz) sposb
zaprezentowany wyej moe si wydawa dziwny. Istnieje te nieco inna metoda na
odseparowanie waciwego rezultatu od informacji o bdzie.
Ot parametry funkcji pozostawiamy bez zmian, natomiast inny bdzie typ zwracany
przez ni. W miejsce pojedynczej wartoci (jak poprzednio: unsigned) uyjemy
struktury:
struct RESULT
{
unsigned uWynik;
bool bBlad;
Zaawansowane C++ 438
};
Zmodyfikowany prototyp bdzie wic wyglda tak:
RESULT StrToUInt(const std::string& strLiczba);
Myl, e nietrudno zgadn, jakie zmiany zajd w treci funkcji.
Wywoanie tak spreparowanej funkcji nie odbiega od wywoania funkcji z wymieszanym
rezultatem. Musi ono wyglda co najmniej tak:
RESULT Wynik = StrToUInt(strLiczba);
if (Wynik.bBlad)
/* bd */
Mona te uy warunku:
if ((Wynik = StrToUInt(strLiczba)).bBlad)
ktry wyglda pewnie dziwnie, ale jest skadniowo poprawny, bo przecie wynikiem
przypisania jest zmienna typu RESULT.
Tak czy inaczej, nie jest to zbyt pocigajca droga. Jest jeszcze gorzej, jeli
uwiadomimy sobie, e dla kadego moliwego typu rezultatu naleaoby definiowa
odrbn struktur. Poza tym prototyp funkcji staje si mniej czytelny, jako e typ jej
waciwego rezultatu (unsigned) ju w nim nie wystpuje.
117
Dlatego te o wiele lepiej uywa metody z dodatkowym parametrem wskanikowym.
Niezbyt dobre wyjcia
Oba zaprezentowane w poprzednim paragrafie sposoby obsugi bdw zakaday proste
poinformowanie wywoujcego funkcj o zainstniaym problemie. Mimo tej prostoty,
sprawdzaj si one bardzo dobrze.
Istniej aczkolwiek take inne metody raportowania bdw, ktre nie maj ju tak
licznych zalet i nie s szeroko stosowane w praktyce. Oto te metody.
Wywoanie zwrotne
Idea wywoania zwrotnego (ang. callback) jest nieskomplikowana. Jeeli w pisanej
przez nas funkcji zachodzi sytuacja wyjtkowa, wywoujemy inn funkcj pomocniczn.
Taka funkcja moe peni rol ratunkow i sprbowa naprawi okolicznoci, ktre
doprowadziy do powstania problemu - jak np. bdne argumenty dla naszej funkcji. W
ostatecznoci moe to by tylko sposb na powiadomienie o nienaprawialnej sytuacji
wyjtkowej.
Uwaga o wygodnictwie
Zaleta wywoania zwrotnego uwidacznia si w powyszym opisie. Przy jego pomocy nie
jestemy skazani na bierne przyjcie do wiadomoci wystpienia bdu; przy odrobinie
dobrej woli mona postara si go naprawi.
Nie zawsze jest to jednak moliwe. Mona wprawdzie poprawi nieprawidowy parametr,
przekazany do funkcji, ale ju nic nie zaradzimy chociaby na brak pamici.
117
Wykorzystanie szablonw zlikwidowaoby obie te niedogodnoci, ale czy naprawd s one tego warte?
Wyjtki 439
Poza tym, technika callback z gry czyni pesymistyczne zaloenie, e sytuacje wyjtkowe
bd trafiay si na tyle czsto, e konieczny staje si mechanizm wywoa zwrotnych.
Jego stosowanie nie zawsze jest wspmierne do problemu, czasem jest to zwyczajne
strzelanie z armaty do komara. Przykadowo, w funkcji Pierwiastek() spokojnie
moemy sobie pozwoli na inne sposoby informowania o bdach - nawet w obliczu faktu,
e naprawienie nieprawidowego argumentu byoby przecie moliwe. Funkcja ta nie jest
bowiem na tyle kosztowna, aby opacao si chroni j przed niespodziewanym
zakoczeniem.
Dlaczego jednak wywoanie zwrotne jest taki cikim rodkiem? Ot wymaga ono
specjalnych przygotowa. Od strony programisty-klienta obejmuj one przede wszystkim
napisania odpowiednich funkcji zwrotnych. Od strony piszcego kod biblioteczny
wymagaj natomiast gruntowego obmylenia mechanizmu takich funkcji zwrotnych: tak,
aby nie mnoy ich ponad miar, a jednoczenie zapewni dla siebie pewn wygod i
uniwersalno.
Uwaga o logice
Funkcje callback s te bardzo kopotliwe z punktu widzenia logiki programu i jego
konstrukcji. Zakadaj bowiem, by kod niszego poziomu - jak funkcje biblioteczne w
rodzaju wspomnianej Pierwiastek() lub StrToUInt() - wywoyway kod wyszego
poziomu, zwizany bezporednio z dziaaniem samej aplikacji. amie to naturaln
hierarchi warstw kodu i burzy porzdek jego wykonywania.
Uwaga o niedostatku mechanizmw
Wreszcie trzeba wspomnie, e w C++ nie ma dobrych sposobw na realizacj funkcji
zwrotnych. Owszem, mamy wskaniki na funkcje - jednak one pozwalaj pokazywa
jedynie na funkcje globalne lub statyczne metody klas. Nie posiadamy natomiast
niezbdnego w programowaniu obiektowym mechanizmu wskanika na niestatyczn
metod obiektu (ang. closure, spotyka si te nazw delegat - ang. delegate), przez
co trudno jest zrealizowa callback.
W poprzednim rozdziale opisaem pewien sposb na obejcie tego problemu, ale jak
wszystkie poowiczne rozwizania, nie jest on zbyt elegancki
Zakoczenie programu
Wyjtkowy bd moe spowodowa jeszcze jedn moliw akcj: natychmiastowe
zakoczenie dziaania programu.
Brzmi to bardzo drastycznie i takie jest w istocie. Naprawd trudno wskaza sytuacj, w
ktrej byoby konieczne przerwanie wykonywania aplikacji - zwaszcza niepoprzedzone
adnym ostrzeeniem czy zapytaniem do uytkownika. Chyba tylko krytyczne braki
pamici lub niezbdnych plikw mog by tego czciowym usprawiedliwieniem.
Na pewno jednak fatalnym pomysem jest stosowanie tego rozwizania dla kadej
sytuacji wyjtkowej. I chyba nawet nie musz mwi, dlaczego
Wyjtki
Takie s tradycyjne sposobu obsugi sytuacji wyjtkowych. Byy one przydatne przez
wiele lat i nadal nie straciy nic ze swojej uytecznoci. Nie myl wic, e mechanizm,
ktry zaraz poka, moe je cakowicie zastpi.
Tym mechanizmem s wyjtki (ang. exceptions). Skojarzenie tej nazwy z sytuacjami
wyjtkowymi jest jak najbardziej wskazane. Wyjtki su wanie do obsugi
niecodzienych, niewystpujcych w normalnym toku programu wypadkw.
Spjrzmy wic, jak moe si to odbywa w C++.
Zaawansowane C++ 440
Rzucanie i apanie wyjtkw
Technik obsugi wyjtkw mona streci w trzech punktach, ktre od razu wska nam
jej najwaniejsze elementy. Tak wic, te trzy zaoenia wyjtkw s nastpujce:
jeeli piszemy kod, w ktrym moe zdarzy si co wyjtkowego i niecodziennego,
czyli po prostu sytuacja wyjtkowa, oznaczamy go odpowiednio. Tym
oznaczeniem jest ujcie kodu w blok try (sprbuj). To cakiem obrazowa nazwa:
kod wewntrz tego bloku nie zawsze moe by poprawnie wykonany, dlatego
lepiej jest mwi o prbie jego wykonania: jeeli si ona powiedzie, to bardzo
dobrze; jeeli nie, bdziemy musieli co z tym fantem zrobi
zamy, e wykonuje si nasz kod wewntrz bloku try i stwierdzamy w nim, e
zachodzi sytuacja wyjtkowa, ktr naley zgosi. Co robimy? Ot uywamy
instrukcji throw (rzu), podajc jej jednoczenie tzw. obiekt wyjtku
(ang. exception object). Ten obiekt, mogcy by dowolnym typem danych, jest
zwykle informacj o rodzaju i miejscu zainstniaego bdu
rzucenie obiektu wyjtku powoduje przerwanie wykonywania bloku try, za nasz
rzucony obiekt leci sobie przez chwil - a zostanie przez kogo zapany. Tym
za zajmuje si blok catch (zap), nastpujcy bezporednio po bloku try. Jego
zadaniem jest reakcja na sytuacj wyjtkow, co zazwyczaj wie si z
odczytaniem obiektu wyjtku (rzuconego przez throw) i podjciem jakiej
sensownej akcji
A zatem mechanizmem wyjtkw dz te trzy proste zasady:
Blok try obejmuje kod, w ktrym moe zaj sytuacja wyjtkowa.
Instrukcja throw wewntrz bloku try suy do informowania o takiej sytuacji przy
pomocy obiektu wyjtku.
Blok catch przechwytuje obiekty wyrzucone przez throw i reaguje na zainstaniae
sytuacje wyjtkowe.
Tak to wyglda w teorii - teraz czas na obejrzenie kodu obsugi wyjtkw w C++.
Blok try-catch
Obsuga sytuacji wyjtkowych zawiera si wewntrz blokw try i catch. Wygldaj one
na przykad tak:
try
{
ryzykowne_instrukcje
}
catch (...)
{
kod_obsugi_wyjtkw
}
ryzykowne_instrukcje zawarte wewntrz bloku try s kodem, ktry poddawany jest
pewnej specjalnej ochronie na wypadek wystpienia wyjtku. Na czym ta ochrona polega
- bdziemy mwi w nastpnym podrozdziale. Na razie zapamitaj, e w bloku try
umieszczamy kod, ktrego wykonanie moe spowodowa sytuacj wyjtkow, np.
wywoania funkcji bibliotecznych.
Jeeli tak istotnie si stanie, to wwczas sterowanie przenosi si do bloku catch.
Instrukcja catch apie wystpujce wyjtki i pozwala przeprowadzi ustalone dziaania
w reakcji na nie.
Wyjtki 441
Instrukcja throw
Kiedy wiadomo, e wystpia sytuacja wyjtkowa? Ot musi ona zosta
zasygnalizowana przy pomocy instrukcji throw:
throw obiekt;
Wystpienie tej instrukcji powoduje natychmiastowe przerwanie normalnego toku
wykonywania programu. Sterowanie przenosi si wtedy do najbliszego pasujcego bloku
catch.
Rzucony obiekt peni natomiast funkcj informujc. Moe to by warto dowolnego
typu - rwnie bdca obiektem zdefiniowanej przez nas klasy, co jest szczeglnie
przydatne. obiekt zostaje wyrzucony poza blok try; mona to porwna do pilota
katapultujcego si z samolotu, ktry niechybnie ulegnie katastrofie. Wystpienie throw
jest bowiem sygnaem takiej katastrofy - sytuacji wyjtkowej.
Wdrwka wyjtku
Zaraz za blokiem try nastpuje najczciej odpowiednia instrukcja catch, ktra zapie
obiekt wyjtku. Wykona potem odpowiednie czynnoci, zawarte w swym bloku, a
nastpnie program rozpocznie wykonywanie dalszych instrukcji, zaraz za blokiem
catch.
Jeli jednak wyjtek nie zostanie przechwycony, to moe on opuci sw macierzyst
funkcj i dotrze do tej, ktr j wywoaa. Jeli i tam nie znajdzie odpowiadajcego
bloku catch, to wyjdzie jeszcze bardziej na powierzchni. W przypadku gdy i tam nie
bdzie pasujcej instrukcji catch, bdzie wyskakiwa jeszcze wyej, i tak dalej.
Proces ten nazywamy odwijaniem stosu (ang. stack unwinding) i trwa on dopki jaka
instrukcja catch nie zapie leccego wyjtku. W skrajnym (i nieprawidowym) przypadku,
odwijanie moe zakoczy si przerwaniem dziaania programu - mwimy wtedy, e
wystpi niezapany wyjtek (ang. uncaught exception).
Schemat 39. Wdrwka wyjtku rzuconego w funkcji
Zarwno o odwijaniu stosu, jak i o apaniu i niezapaniu wyjtkw bdziemy szerzej
mwi w przyszym podrozdziale.
throw a return
Instrukcja throw jest troch podobna do instrukcji return, ktrej uywamy do
zakoczenia funkcji i zwrcenia jej rezultatu. Istniej jednak wane rnice:
return powoduje zawsze przerwanie tylko jednej funkcji i powrt do miejsca, z
ktrego j wywoano. throw moe natomiast wcale nie przerywa wykonywania
Zaawansowane C++ 442
funkcji (jeeli znajdzie w niej pasujc instrukcj catch), lecz rwnie dobrze moe
przerwa dziaanie wielu funkcji, a nawet caego programu
w przypadku return moliwe jest rzucenie obiektu nalecego tylko do jednego,
cile okrelonego typu. Tym typem jest typ zwracany przez funkcj, okrelany w
jej deklaracji. throw moe natomiast wyrzuca obiekt dowolnego typu, zalenie
od potrzeb
return jest normalnym sposobem powrotu z funkcji, ktry stosujemy we
wszystkich typowych sytuacjach. throw jest za uywany w sytuacjach
wyjtkowych; nie powinno si uywa go jako zamiennika dla return, bo
przeznaczenie obu tych instrukcji jest inne
Wida wic, e mimo pozornego podobiestwa instrukcje te s zupenie rne. return
jest typow instrukcj jzyka programowania, bez ktrej tworzenie programw byoby
niemoliwe. throw jest z kolei czci wikszej caloci - mechanizmu obsugi wyjtkw -
bdcym po prostu specjalnym mechanizmem radzenia sobie z sytuacjami kryzysowymi.
Mimo jej przydatnoci, stosowanie tej techniki nie jest obowizkowe.
Skoro jednak mamy wybiera midzy uywaniem a nieuywaniem wyjtkw (a takich
wyborw bdziesz dokonywa czsto), naley wiedzie o wyjtkach co wicej. Dlatego
te kontynuujemy zajmowanie si tym tematem.
Waciwy chwyt
W poprzednich akapitach kilkakrotnie uywaem sformuowania pasujcy blok catch
oraz odpowiednia instrukcja catch. C one znacz?
Jedn z zalet mechanizmu wyjtkw jest to, e instrukcja throw moe wyrzuca obiekty
dowolnego typu. Ponisze wiersze s wic cakowicie poprawne:
throw 42u;
throw "Straszny blad!";
throw CException("Wystapil wyjatek", __FILE__, __LINE__);
throw 17.5;
Te cztery instrukcje throw rzucaj (odpowiednio) obiekty typw unsigned, const
char[], zdefiniowanej przez uytkownika klasy CException oraz double. Wszystkie one
s zapewne cennymi informacjami o bdach, ktre naleaoby odczyta w bloku catch.
Niewykluczone przecie, e nawet najmniejsza pomoc z miejsca katastrofy moe by
dla nas przydatna.
Dlatego te w mechanizmie wyjtkw przewidziano sposb nie tylko na oddanie
sterowania do bloku catch, ale te na przesanie tam jednego obiektu. Jest to oczywicie
ten obiekt, ktry podajemy instrukcji throw.
catch otrzymuje natomiast jego lokaln kopi - w podobny sposb, w jaki funkcje
otrzymuj kopie przekazanych im parametrw. Aby jednak tak si stao, blok catch musi
zadeklarowa, z jakiego typu obiektami chce pracowa:
catch (typ obiekt)
{
kod
}
W ten sposb bedzie mia dostp do kadego zapanego obiektu wyjtku, ktry naley
do podanego typu. Da mu to moliwo wykorzystania go - chociaby po to, aby
wywietli uytkownikowi zawarte w nim informacje:
Wyjtki 443
try
{
srand (static_cast<unsigned>(time(NULL)))
// losujemy rzucony wyjtek
switch (rand() % 4)
{
case 0: throw "Wyjatek tekstowy";
case 1: throw 1.5f; // wyjtek typu float
case 2: throw -12; // wyjtek typu int
case 3: throw (void*) NULL; // pusty wskanik
}
}
catch (int nZlapany)
{
std::cout << "Zlapalem wyjatek liczbowy z wartoscia " << nZlapany;
}
Komunikaty o bdach powinny by w zasadzie kierowane do strumienia cerr, a nie
cout. Tutaj jednak, dla zachowania prostoty, bd posugiwa si standardowym
strumieniem wyjcia. O pozostaych dwch rodzajach strumieni wyjciowych pomwimy
w rozdziale o strumieniach STL.
W tym kawaku kodu blok catch zapie liczb typu int - jeeli takowa zostanie
wyrzucona przez instrukcj throw. Przechwyci j w postaci lokalnej zmiennej nZlapany,
aby potem wywietli jej warto w konsoli.
A co z pozostaymi wyjtkami? Nie mamy instrukcji catch, ktre by je apay. Wobec
tego zostan one wyrzucone ze swej macierzystej funkcji i bd wdroway t ciek a
do natrafienia pasujcych blokw catch. Jeeli ich nie znajd, spowoduj zakoczenie
programu.
Powinnimy zatem zapewni obsug take i tych wyjtkw. Robimy w taki sposb, i
dopisujemy po prostu brakujce bloki catch:
catch (const char szNapis[])
{
std::cout << szNapis;
}
catch (float fLiczba)
{
std::cout << "Zlapano liczbe: " << fLiczba;
}
catch (void* pWskaznik)
{
std::cout << "Wpadl wskaznik " << pWskaznik;
}
Blokw catch, nazywanych procedurami obsugi wyjtkw (ang. exception handlers),
moe by dowolna ilo. Wszystko zaley od tego, ile typw wyjtkw zamierzamy
przechwytywa.
Kolejno blokw catch
Obecno kilku blokw catch po jednej instrukcji try to powszechna praktyka. Dziki
niej mona bowiem zabezpieczy si na okoliczno rnych rodzajw wyjtkw. Warto
wic o tym porozmawia.
Zaawansowane C++ 444
Dopasowywanie typu obiektu wyjtku
Zamy wic, e mamy tak oto sekwencj try-catch:
try
{
// rzucamy wyjtek
throw 90;
}
catch (float fLiczba) { /* ... */ }
catch (int nLiczba) { /* ... */ }
catch (double fLiczba) { /* ... */ }
W bloku try rzucamy jako wyjtek liczb 90. Poniewa nie podajemy jej adnych
przyrostkw, kompilator uznaje, i jest to warto typu int. Nasz obiekt wyjtku jest
wic obiektem typu int, ktry leci na spotkanie swego losu.
Gdzie si zakoczy jego droga? Wszystko zaley od tego, ktry z trzech blokw catch
przechwyci ten wyjtek. Wszystkie one s do tego zdolne: typ int pasuje bowiem
zarwno do typu float, jak i double (no i oczywicie int).
Mwic pasuje, mam tu na myli dokadnie taki sam mechanizm, jaki jest uruchamiany
przy wywoywaniu funkcji z parametrami. Majc bowiem trzy funkcje:
void Funkcja1(float);
void Funkcja2(int);
void Funkcja3(double);
kadej z nich moemy przekaza warto typu int. Naturalnie, jest on najbardziej
zgodna z Funkcja2(), ale pozostae te si do tego nadaj. W ich przypadku zadziaaj
po prostu wbudowane, niejawne konwersje: kompilator zamieni liczb na int na typ
float lub double.
A jednak to tylko cz prawdy. Zgodno typu wyjtku z typem zadeklarowanym w
bloku catch to tylko jedno z kryterium wyboru - w dodatku wcale nie najwaniejsze!
Ot najpierw w gr wchodzi kolejno instrukcji catch. Kompilator przeglda je w takim
samym porzdku, w jakim wystpuj w kodzie, i dla kadej z nich wykonuje test
dopasowania argumentu. Jeli stwierdzi jakkolwiek zgodno (niekoniecznie
najlepsz moliw), ignoruje wszystkie pozostae bloki catch i wybiera ten pierwszy
pasujcy.
Co to znaczy w praktyce? Spjrzmy na nasz przykad. Mamy obiekt typu int, ktry
zostanie kolejno skonfrontowany z typami trzech blokw catch: float, int i double.
Wobec przedstawionych wyej zasad, ktry z nich zostanie wybrany?
Odpowied nie jest trudna. Ju pierwsze dopasowanie int do float zakoczy si
sukcesem. Nie bdzie ono wprawdzie najlepsze (wymaga bdzie niejawnej konwersji),
ale, jak podkresliem, kompilator poprzestanie wanie na nim. Porzdek blokw catch
wemie po prostu gr nad ich zgodnoci.
Pamitaj wic zasad dopasowywania typu obiektu rzuconego do wariantw catch:
Typy w blokach catch s sprawdzane wedle ich kolejnoci w kodzie, a wybierana jest
pierwsza pasujca moliwo. Przy dopasowywaniu brane s pod uwag wszystkie
niejawne konwersje.
Szczeglnie natomiast we sobie do serca, i:
Kolejno blokw catch czsto ma znaczenie.
Wyjtki 445
Mimo e z pozoru przypominaj one funkcje, funkcjami nie s. Obowizuj w nich wic
inne zasady wyboru waciwego wariantu.
Szczegy przodem
Jak w takim razie naley ustawia procedury obsugi wyjtkw, aby dziaay one zgodnie
z naszymi yczeniami? Popatrzmy wpierw na taki przykad:
try
{
// ...
throw 16u; // unsigned
// ...
throw -87; // int
// ...
throw 9.242f; // float
// ...
throw 3.14157; // double
}
catch (double fLiczba) { /* ... */ }
catch (int nLiczba) { /* ... */ }
catch (float fLiczba) { /* ... */ }
catch (unsigned uLiczba) { /* ... */ }
Pytanie powinno tutaj brzmie: co jest le na tym obrazku? Domylasz si, e chodzi o
kolejno blokw catch. Sprawdmy.
W bloku try rzucamy jeden z czterech wyjtkw - typu unsigned, int, float oraz
double. Co si z nimi dzieje? Oczywicie trafiaj do odpowiednich blobkw catch czy
aby na pewno?
Niezupenie. Wszystkie te liczby zostan bowiem od razu dopasowane do pierwszego
wariantu z parametrem double. Typ double swobodnie potrafi pomieci wszystkie
cztery typy liczbowe, zatem wszystkie cztery wyjtkie trafi wycznie do pierwszego
bloku catch! Pozostae trzy s w zasadzie zbdne!
Kolejno procedur obsugi jest zatem nieprawidowa. Poprawnie powinny by one
uoone w ten sposb:
catch (unsigned uLiczba) { /* ... */ }
catch (int nLiczba) { /* ... */ }
catch (float fLiczba) { /* ... */ }
catch (double fLiczba) { /* ... */ }
To gwarantuje, e wszystkie wyjtki trafi do tych blokw catch, ktre im dokadnie
odpowiadaj. Korzystamy tu z faktu, e:
typ unsigned w pierwszym bloku przyjmie tylko wyjtki typu unsigned
typ int w drugim bloku mgby przej zarwno liczby typu unsigned, jak i int.
Te pierwsz s jednak przechwycane przez poprzedni blok, zatem tutaj trafiaj
wycznie wyjtki faktycznego typu int
typ float moe przyj typy unsigned, int i float. Pierwsze dwa s ju jednak
obsuone, wic ten blok catch dostaje tylko prawdziwe liczby
zmiennoprzecinkowe pojedynczej precyzji
typ double pasuje do kadej liczby, ale tutaj blok catch z tym typem dostanie
jedynie te wyjtki, ktre s faktycznie typu double. Pozostae liczby zostan
przechwycone przez poprzednie warianty
Midzy typami unsigned, int, float i double zachodzi tu po prosta relacja polegajca
na tym, e kady z nich jest szczeglnym przypadkiem nastpnego:
Zaawansowane C++ 446
unsigned int float double
Najbardziej szczeglny jest typ unsigned i dlatego on wystpuje na pocztku. Dalej
mamy ju coraz bardziej oglne typy liczbowe.
Taka zasada konstrurowania sekwencji blokw catch jest poprawna w kadym
przypadku, nie tylko dla typw liczbowych,
Umieszczajc kilka blokw catch jeden po drugim, zadbaj o to, aby wystpoway one w
porzdku rosncej oglnoci. Niech najpierw pojawi si bloki o najbardziej
wyspecjalizowanych typach, a dopiero potem typy coraz bardziej oglne.
Moesz krci nosem na takie niecise sformulowania. Bo i co to znaczy, e dany typ jest
oglniejszy ni inny? W gr wchodz tu niejawne konwersje - jak wiemy, kompilator
stosuje je przy dopasowywaniu w blokach catch. Mona zatem powiedzie, e:
Typ A jest oglniejszy od typu B, jeeli istnieje niejawna konwersja z B do A,
niepowodujca utraty danych.
W tym sensie double jest oglniejszy od kadego z typw: unsigned, int i float,
poniewa w kadym przypadku istniej niejawne konwersje standardowe, zamieniajce
te typy na double. To zreszt zgodne ze zdrowym rozsdkiem i wiedz matematyczn,
ktra mwi, nam e liczby naturalne i cakowite s take liczbami rzeczywistymi.
Innym rodzajem konwersji, ktry bdzie nas interesowa w tym rozdziale, jest zamiana
odwoania do obiektu klasy pochodnej na odwoanie do obiektu klasy bazowej. Uyjemy
jej do budowy hierarchii klas dla wyjtkw.
Zagniedone bloki try-catch
Wewntrz bloku try moe znale si dowolny kod, jaki moe by umieszczany we
wszystkich blokach instrukcji C++. Przypisania, instrukcje warunkowe, ptle, wywoania
funkcji - wszystko to jest dopuszczalne. Co wicej, w bloku try mog si znale inne
bloki try-catch. Nazywami je wtedy zagniedonymi, zupenie tak samo jak
zagniedone instrukcje if czy ptle.
Formalnie skadnia takiego zagniedenia moe wyglda tak:
try
{
try
{
ryzykowne_instrukcje_wewntrzne
}
catch (typ_wewntrzny_1 obiekt_wewntrzny_1)
{
wewntrzne_instrukcje_obsugi_1
}
catch (typ_wewntrzny_2 obiekt_wewntrzny_2)
{
wewntrzne_instrukcje_obsugi_2
}
// ...
ryzykowne_instrukcje_zewntrzne
}
catch (typ_zewntrzny_1 obiekt_zewntrzny_1)
{
zewntrzne_instrukcje_obsugi_1
Wyjtki 447
}
catch (typ_zewntrzny_1 obiekt_zewntrzny_2)
{
zewntrzne_instrukcje_obsugi_2
}
// ...
dalsze_instrukcje
Mimo pozornego skomplikowania jej funkcjonowanie jest intuicyjne. Jeeli podczas
wykonywania ryzykownych_instrukcji_wewntrznych rzucony zostanie wyjtek, to
wpierw bdzie on apany przez wewntrzne bloki catch. Dopiero gdy one przepuszcz
wyjtek, do pracy wezm si bloki zewntrzne.
Jeeli natomiast ktry z zestaww catch (wewntrzny lub zewntrzny) wykona swoje
zadanie, to program bdzie kontynuowa od nastpnych linijek po tym zestawie. Tak wic
w przypadku, gdy wyjtek zapie wewntrzny zestaw, wykonywane bd
ryzykowne_instrukcje_zewntrzne; jeli zewntrzny - dalsze_instrukcje.
No a jeli aden wyjtek nie wystpi? Wtedy wykonaj si wszystkie instrukcje poza
blokami catch, czyli: ryzykowne_instrukcje_wewntrzne,
ryzykowne_instrukcje_zewntrzne i wreszcie dalsze_instrukcje.
Takie dosowne zagniedanie blokw try-catch jest w zasadzie rzadkie. Czciej
wewntrzny blok wystpuje w funkcji, ktrej wywoanie mamy w zewntrznym bloku.
Oto przykad:
void FunkcjaBiblioteczna()
{
try
{
// ...
}
catch (typ obiekt)
{
// ...
}
// ...
}
void ZwyklaFunkcja()
{
try
{
FunkcjaBiblioteczna();
// ...
}
catch (typ obiekt)
{
// ...
}
}
Takie rozwizanie ma prost zalet: FunkcjaBiblioteczna() moe zapa i obsuy te
wyjtki, z ktrymi sama sobie poradzi. Jeeli nie potrzeba angaowa w to wywoujcego,
jest to dua zaleta. Cz wyjtkw najprawdopodobniej jednak opuci funkcj - tylko
tymi bdzie musia zaj si wywoujcy. Wewntrzne sprawy wywoywanej funkcji
(take wyjtki) pozostan jej wewntrznymi sprawami.
Oglnie mona powiedzie, e:
Zaawansowane C++ 448
Wyjtki powinny by apane w jak najbliszym od ich rzucenia miejscu, w ktrym
moliwe jest ich obsuenie.
O tej wanej zasadzie powiemy sobie jeszcze przy okazji uwag o wykorzystaniu
wyjtkw.
Zapanie i odrzucenie
Przy zagniedaniu blokw try (niewane, czy z porednictwem funkcji, czy nie) moe
wystpi czsta w praktyce sytuacja. Moliwe jest mianowicie, e po zapaniu wyjtku
przez bardziej wewntrzny catch nie potrafimy podj wszystkich akcji, jakie byyby
dla niego konieczne. Przykadowo, moemy tutaj jedynie zarejestrowa go w dzienniku
bdw; bardziej uyteczn reakcj powinien zaj si kto wyej.
Moglibymy pomin wtedy ten wewntrzny catch, ale jednoczenie pozbawilibymy si
moliwoci wczesnego zarejestrowania bdu. Lepiej wic pozostawi go na miejscu, a po
zakoczeniu zapisywania informacji o wyjtku wyrzuci go ponownie. Robimy to
instrukcj throw bez adnych parametrw:
throw;
Ta instrukcja powoduje ponowne rzucenie tego samego obiektu wyjtku. Teraz jednak
bd mogy zaj si nim bardziej zewntrzne bloki catch. Bd one pewnie bardziej
kompetentne ni nasze siy szybkiego reagowania.
Blok catch(...), czyli chwytanie wszystkiego
W poczeniu z zagniedonymi blokami try i instrukcj throw; czesto wystpuje
specjalny rodzaj bloku catch. Nazywany jest on uniwersalnym, a powstaje poprzez
wpisanie po catch wielokropka (trzech kropek) w nawiasie:
try
{
// instrukcje
}
catch (...)
{
// obsuga wyjtkw
}
Uniwersalno tego specjalnego rodzaju catch polega na tym, i pasuj do niego
wszystkie obiekty wyjtkw. Jeeli kompilator, transportujc wyjtek, natrafi na
catch(...), to bezwarunkowo wybierze wanie ten wariant, nie ogldajc si na
adne inne. catch(...) jest wic wszystkoerny: pochania dowolne typy wyjtkw.
Pochania to zreszt dobre sowo. Wewntrz bloku catch(...) nie mamy mianowicie
adnych informacji o obiekcie wyjtku. Nie tylko o jego wartoci, ani nawet o jego typie.
Wiemy jedynie, e jaki wyjtek wystpi - i skromn t wiedz musimy si zadowoli.
Po co nam wobec tego taki dziwny blokcatch? Jest on przydatny tam, gdzie moemy
jako wykorzysta samo powiadomienie o wyjtku, nie znajc jednak jego typu ani
wartoci. Wewntrz catch(...) moemy jedynie podja pewne domylne dziaania.
Moemy na przykad dokona maego zrzutu pamici (ang. memory dump), zapisujc w
bezpiecznym miejscu wartoci zmiennych na wypadek zakoczenia programu. Moemy
te w jaki sposb przygotowa si do waciwej obsugi bdw.
Cokolwiek zrobimy, na koniec powinnimy przekaza wyjtek dalej, czyli uy
konstrukcji:
throw;
Wyjtki 449
Jeeli tego nie zrobimy, to catch(...) zdusi w zarodku wszelkie wyjtki, nie pozwalajc
na to, by dotary one dalej.
***
Na tym kocz si podstawowe informacje o mechanizmie wyjtkw. To jednak nie
wszystkie aspekty tej techniki. Musimy sobie jeszcze porozmawia o tym, co dzieje si
midzy rzuceniem wyjtku poprzez throw i jego zapaniem przy pomocy catch.
Porozmawiamy zatem o odwijaniu stosu.
Odwijanie stosu
Odwijanie stosu (ang. stack unwinding) jest procesem cile zwizanym z wyjtkami.
Jakkolwiek sama jego istota jest raczej prosta, musimy wiedze, jakie ma on
konsekwencje w pisanym przez nas kodzie.
Midzy rzuceniem a zapaniem
Odwijanie stosu rozpoczyna si wraz z rzuceniem jakiegokolwiek wyjtku przy pomocy
instrukcji throw i postpuje a do momentu natrafienia na pasujcy do niego blok catch.
W skrajnym przypadku odwijanie moe doprowadzi do zakoczenia dziaania programu -
jest tak jeli odpowiednia procedura obsugi wyjtku nie zostanie znaleziona.
Wychodzenie na wierzch
Na czym jednak polega samo odwijanie? Ot mona opisa je w skrcie jako
wychodzenie punktu wykonania ze wszystkich blokw kodu. Co to znaczy,
najlepiej wyjani na przykadzie.
Zamy, e mamy tak oto sytuacj:
try
{
for (/* ... */)
{
switch (/* ... */)
{
case 1:
if (/* ... */)
{
// ...
throw obiekt;
}
}
}
}
catch
{
// ...
}
Instrukcja throw wystpuje to wewntrz 4 zagniedonych w sobie blokw: try, for,
switch i if. My oczywicie wiemy, e najwaniejszy jest ten pierwszy, bo zaraz za nim
wystpuje procedura obsugi wyjtku - catch.
Co si dzieje z wykonywaniem programu, gdy nastpuje sytuacja wyjtkowa? Ot nie
skacze on od razu do odpowiedniej instrukcji catch. Byoby to moe najszybsze z
Zaawansowane C++ 450
punktu widzenia wydajnoci, ale jednoczenie cakowicie niedopuszczalne. Dlaczego tak
jest - o tym powiemy sobie w nastpnym paragrafie.
Jak wic postpuje kompilator? Rozpoczyna to sawetne odwijanie stosu, ktremu
powicony jest cay ten podrozdzia. Dziaa to mniej wicej tak, jakby dla kadego
bloku, w ktrym si aktualnie znajdujemy, zadziaaa instrukcja break. Powoduje to
wyjcie z danego bloku.
Po kadej takiej operacji jest poza tym sprawdzana obecno nastpujcego dalej bloku
catch. Jeeli takowy jest obecny, i pasuje on do typu obiektu wyjtku, to wykonywana
jest procedura obsugi wyjtku w nim zawarta. Proste i skuteczne :)
Zobaczmy to na naszym przykadzie. Instrukcja throw znajduje si tu przede wszystkim
wewntrz bloku if - i to on bdzie w pierwszej kolejnoci odwinity. Potem nie zostanie
znaleziony blok catch, zatem opuszczone zostan take bloki switch, for i wreszcie try.
Dopiero w tym ostatnim przypadku natrafimy na szukan procedur obsugi, ktra
zostanie wykonana.
Warto pamita, e - cho nie wida tego na przykadzie - odwijanie moe te dotyczy
funkcji. Jeeli zajdzie konieczno odwinicia jej bloku, to sterowanie wraca do
wywoujcego funkcj.
Porwnanie throw z break i return
Nieprzypadkowo porwnaem instrukcj throw do break, a wczeniej do return. Czas
jednak zebra sobie cechy wyrniajce i odrniajce te trzy instrukcje. Oto stosowna
tabela:
instrukcja
cecha
throw break return
przekazywanie
sterowania
do najbliszego
pasujcego bloku
catch
jeden blok wyej
(wyjcie z ptli lub
bloku switch)
zakoczenie dziaania
funkcji i powrt do kodu,
ktry j wywoa
warto
obiekt wyjtku
dowolnego typu
nie jest zwizana z
adn wartoci
warto tego samego
typu, jaki zosta
okrelony w deklaracji
funkcji
zastosowanie
obsuga sytuacji
wyjtkowych
oglne programowanie
Tabela 20. Porwnanie throw z break i return
Wszystkie te trzy wasnoci trzech instrukcji s bardzo wane i koniecznie musisz o nich
pamita. Nie bdzie to chyba dla ciebie problemem, skoro dwie z omawianych instrukcji
znasz doskonale, a o wszystkich aspektach trzeciej porozmawiamy sobie jeszcze cakiem
obszernie.
Wyjtek opuszcza funkcj
Rzucenie oraz zapanie i obsuga wyjtku moe odbywa si w ramach tej samej funkcji.
Czsto jednak mamy sytuacj, w ktrej to jedna funkcja sygnalizuje sytuacj wyjtkow,
a dopiero inna (wywoujca j) zajmuje si reakcj na zainstniay problem. Jest to
zupenie dopuszczalne, co zreszt parokrotnie podkrelaem.
W procesie odwijania stosu obiekt wyjtku moe wic opuci swoj macierzyst funkcj.
Nie jest to aden bd, lecz normalna praktyka. Nie zwalnia ona jednak z obowizku
zapania wyjtku: nadal kto musi to zrobi. Kto - czyli wywoujcy funkcj.
Wyjtki 451
Specyfikacja wyjtkw
Aby jednak mona byo to uczyni, naley wiedzie, jakiego typu wyjtki funkcja moe
wyrzuca na zewntrz. Dziki temu moemy opakowa jej przywoanie w blok try i
doda za nim odpowiednie instrukcje catch, chwytajce waciwe obiekty.
Skd mamy uzyska t tak potrzebn wiedz? Wydawaoby si, e nic prostszego.
Wystarczy przejrze kod funkcji, znale wszystkie instrukcje throw i okreli typ
obiektw, jakie one rzucaj. Nastpnie naley odrzuci te, ktre s obsugiwane w samej
funkcji i zaj si tylko wyjtkami, ktre z niej uciekaj.
Ale to tylko teoria i ma ona jedn powan sabostk. Wymaga przecie dostpu do kodu
rdowego funkcji, a ten nie musi by wcale osigalny. Wiele bibliotek jest
dostarczanych w formie skompilowanej, zatem nie ma szans na ujrzenie ich wntrza.
Mimo to ich funkcjom nikt cakowicie nie zabroni rzucania wyjtkw.
Dlatego naleao jako rozwiza ten problem. Uzupeniono wic deklaracje funkcji o
dodatkow informacj - specyfikacj wyjtkw.
Specyfikacja albo wyszczeglnienie wyjtkw (ang. exceptions specification) mwi
nam, czy dana funkcja wyrzuca z siebie jakie nieobsuone obiekty wyjtkw, a jeli
tak, to informuje take o ich typach.
Takie wyszczeglnienie jest czci deklaracji funkcji - umieszczamy je na jej kocu, np.:
void Znajdz(int* aTablica, int nLiczba) throw(void*);
Po licie parametrw (oraz ewentualnych dopiskach typu const w przypadku metod
klasy) piszemy po prostu sowo throw. Dalej umieszczamy w nawiasie list typw
wyjtkw, ktre bd opuszczay funkcj i ktrych zapanie bdzie naleao do
obowizkw wywoujcego. Oddzielamy je przecinkami.
Ta lista typw jest nieobowizkowa, podobnie zreszt jak caa fraza throw(). S to
jednak dwa szczeglne przypadki - wygldaj one tak:
void Stepuj();
void Spiewaj() throw();
Brak specyfikacji oznacza tyle, i dana funkcja moe rzuca na zewntrz wyjtki
dowolnego typu. Natomiast podanie throw bez okrelenia typw wyjtkw informuje,
e funkcja w ogle nie wyrzuca wyjtkw na zewntrz. Widzc tak zadeklarowan
funkcj moemy wic mie pewno, e jej wywoania nie trzeba umieszcza w bloku try
i martwi si o obsug wyjtkw przez catch.
Specyfikacja wyjtkw jest czci deklaracji funkcji, zatem bdzie ona wystpowa
np. w pliku nagwkowym zewntrznej biblioteki. Jest to bowiem niezbdna informacja,
potrzebna do korzystania z funkcji - podobnie jak jej nazwa czy parametry. Kiedy jednak
tamte wiadomoci podpowiadaj, w jaki sposb wywoywa funkcj, wyszczeglnienie
throw() mwi nam, jakie wyjtki musimy przy okazji tego wywoania obsugiwa.
Warto te podkreli, e mimo swej obecnoci w deklaracji funkcji, specyfikacja wyjtkw
nie naley do typu funkcji. Do niego nadal zaliczamy wycznie list parametrw oraz
typ wartoci zwracanej. Na pokazane wyej funkcje Stepuj() i Spiewaj() mona wic
pokazywa tym samym wskanikiem.
Kamstwo nie popaca
Specyfikacja wyjtkw jest przyczeniem zoonym przez twrc funkcji jej
uytkownikowi. W ten sposb autor procedury zawiadcza, e jego dzieo bdzie
wyrzucao do wywoujcego wyjtki wycznie podanych typw.
Zaawansowane C++ 452
Niestety, ycie i programowanie uczy nas, e niektre obietnice mog by tylko
obiecankami. Zamy na przykad, e w nowej wersji biblioteki, z ktrej pochodzi
funkcja, dokonano pewnych zmian. Teraz rzucany jest jeszcze jeden, nowy typ wyjtkw,
ktrego obsuga spada na wywoujcego.
Zapomniano jednak zmieni deklaracj funkcji - wyglda ona nadal np. tak:
bool RobCos() throw(std::string);
Obiecywanym typem wyjtkw jest tu tylko i wycznie std::string. Przypumy
jednak, e w wyniku poczynionych zmian funkcja moe teraz rzuca take liczby typu int
- typu, ktrego nazwa nie wystpuje w specyfikacji wyjtkw.
Co si wtedy stanie? Czy wystpi bd? Powiedzmy. Jednak to nie kompilator nam o
nim powie. Nie zrobi tego nawet linker. Ot:
O rzuceniu przez funkcj niezadeklarowanego wyjtku dowiemy si dopiero w
czasie dziaania programu.
Wyglda to tak, i program wywoa wtedy specjaln funkcj unexpected()
(niespodziewany). Jest to funkcja biblioteczna, uruchamiana w reakcji na niedozwolony
wyjtek.
Co robi ta funkcja? Ot wywouje ona drug funkcj, terminate() (przerwij). O niej
bdziemy jeszcze rozmawia przy okazji niezapanych wyjtkw. Na razie zapamitaj, e
funkcja ta po prostu koczy dziaanie programu w mao porzdny sposb.
Wyrzucenie przez funkcj niezadeklarowanego wyjtku koczy si awaryjnym
przerwaniem dziaania programu.
Spytasz pewnie: Dlaczego tak drastycznie? Taka reakcja jest jednak uzasadniona, gdy
do czynienia ze zwyczajnym oszustwem.
Oto kto (twrca funkcji) deklaruje, e bdzie ona wystrzeliwa z siebie wycznie
okrelone typy wyjtkw. My posusznie podporzdkowujemy si tej obietnicy:
ujmujemy wywoanie funkcji w blok try i piszemy odpowiednie bloki catch. Wszystko
robimy zgodnie ze specyfikacj throw().
Tymczasem zostajemy oszukani. Obietnica zostaa zamana: funkcja rzuca nam
wyjtek, ktrego si zupenie nie spodziewalimy. Nie mamy wic kodu jego obsugi -
albo nawet gorzej: mamy go, ale nie tam gdzie trzeba. W kadym przypadku jest to
sytuacja nie do przyjcia i stanowi wystarczajc podstaw do zakoczenia dziaania
programu.
To domylne moemy aczkolwiek zmieni. Nie zaleca si wprawdzie, aby mimo
niespodziewanego wyjtku praca programu bya kontynuowana. Jeeli jednak napiszemy
wasn wersj funkcji unexpected(), bdziemy mogli odrni dwie sytuacje:
niezapany wyjtek - czyli taki wyjtek, ktrego nie schwyci aden blok catch
nieprawidowy wyjtek - taki, ktry nie powinien si wydosta z funkcji
Rnica jest bardzo wana, bowiem w tym drugim przypadku nie jestemy winni
zaistniaemu problemu. Dokadniej mwic, nie jest winny kod wywoujcy funkcj -
przyczyna tkwi w samej funkcji, a zawini jej twrca. Jego obietnice dotyczce wyjtkw
okazay si obietnicami bez pokrycia.
Rozdzielenie tych dwch sytuacji pozwoli nam uchroni si przed poprawianiem kodu,
ktry by moe wcale tego nie wymaga. Z powodu niezadeklarowanego wyjtku nie ma
bowiem potrzeby dokonywania zmian w kodzie wywoujcym funkcj. Pniej bd one
oczywicie konieczne; pniej - to znaczy wtedy, gdy powiadomimy twrc funkcj o
jego niekompetencji, a ten z pokor naprawi swj bd.
Wyjtki 453
Jak zatem moemy zmieni domyln funkcj unexpected()? Czynimy to wywoujc
inn funkcj - set_unexpected():
unexpected_handler set_unexpected(unexpected_handler pfnFunction);
Tym, ktry ta funkcja przyjmuje i zwraca, to unexpected_handler. Jest to alias ta
wskanik do funkcji: takiej, ktra nie bierze adnych parametrw i nie zwraca adnej
wartoci.
Poprawn wersj funkcji unexpected() moe wic by np. taka funkcja:
void MyUnexpected()
{
std::cout << "--- UWAGA: niespodziewany wyjtek ---" << std::endl;
exit (1);
}
Po przekazaniu jej do set_unexpected():
set_unexpected (MyUnexpected);
bdziemy otrzymywali stosown informacj w przypadku wyrzucenia niedozwolonego
wyjtku przez jakkolwiek funkcj programu.
Niezapany wyjtek
Przekonalimy si, e proces odwijania stosu moe doprowadzi do przerwania dziaania
funkcji i poznalimy tego konsekwencje. Nieprawidowe sygnalizowanie lub obsuga
wyjtkw mog nam jednak sprawi jeszcze jedn niespodziank.
Odwijanie moe si mianowicie zakoczy niepowodzeniem, jeli aden pasujcy blok
catch nie zostanie znaleziony. Mwimy wtedy, e wystpi nieobsuony wyjtek.
Co nastpuje w takim wypadku? Ot program wywouje wtedy funkcj terminate(). Jej
nazwa wskazuje, e powoduje ona przerwanie programu. Faktycznie funkcja ta wywouje
inn funkcj - abort() (przesta). Ona za powoduje brutalne i nieznoszce adnych
kompromisw przerwanie dziaania programu. Po jej wywoaniu moemy w oknie konsoli
ujrze komunikat:
Abnormal program termination
Taki te napis bdzie poegnaniem z programem, w ktrym wystpi niezapany wyjtek.
Moemy to jednak zmieni, piszc wasn wersj funkcji terminate().
Do ustawienia nowej wersji suy funkcja set_terminate(). Jest ona bardzo podobna do
analogicznej funkcji set_unexpected():
terminate_handler set_terminate(terminate_handler pfnFunction);
Wystpujcy tu alias terminate_handler jest take wskanikiem na funkcj, ktra nic
nie bierze i nie zwraca. W parametrze set_terminate() podajemy wic wskanik do
nowej funkcji terminate(), a w zamian otrzymujemy wskanik do starej - zupenie jak w
set_unexpected().
Oto przykadowa funkcja zastpcza:
void MyTerminate()
{
std::cout << "--- UWAGA: blad mechanizmu wyjatkow ---" << std::endl;
Zaawansowane C++ 454
exit (1);
}
Wypisywany przez nas komunikat jest tak oglny (nie brzmi np. "niezlapany
wyjatek"), poniewa terminate() jest wywoywana take w nieco innych sytuacjach, ni
niezapany wyjtek. Powiemy sobie o nich we waciwym czasie.
Zastosowana tutaj, jak w i MyUnexpected() funkcja exit() suy do normalnego (a nie
awaryjnego) zamknicie programu. Podajemy jej tzw. kod wyjcia (ang. exit code) -
zwyczajowo zero oznacza wykonanie bez bdw, inna warto to nieprawidowe dziaanie
aplikacji (tak jak u nas).
Porzdki
Odwijanie stosu jest w praktyce bardziej zoonym procesem ni to si wydaje. Oprcz
przetransportowania obiektu wyjtku do stosownego bloku catch kompilator musi
bowiem zadba o to, aby reszta programu nie doznaa przy okazji jakich obrae.
O co chodzi? O tym porozmawiamy sobie w tym paragrafie.
Niszczenie obiektw lokalnych
Wspominajc o opuszczaniu kolejno zagniedonych blokw czy nawet funkcji,
posuyem si porwnaniu z break i return. throw ma z nimi jeszcze jedn cech
wspln - nie liczc tych odrniajcych.
Wychodzenie z blokw przebiega mianowicie w sposb cakiem czysty - tak jak w
normalnym kodzie. Oznacza, to e wszystkie stworzone obiekty lokalne s niszczone,
a ich pami zwalniania.
W przypadku typw podstawowych oznacza to po prostu usunicie zmiennych z pamici.
Dla klas mamy jeszcze wywoywanie destruktorw i wszystkie tego konsekwencje.
Mona zatem powiedzie, e:
Opuszczanie blokw kodu dokonywane podczas odwijania stosu przebiega tak samo, jak
to si dzieje podczas normalnego wykonywania programu. Obiekty lokalne s wic
niszczone poprawnie.
Sama nazwa odwijanie stosu pochodzi zreszt od tego sprztania, dokonywanego przy
okazji wychodzenia na wierzch programu. Obiekty lokalne (zwane te automatycznymi)
s bowiem tworzone na stosie, a jego odwinicie to wanie usunicie tych obiektw oraz
powrt z wywoywanych funkcji.
Wypadki przy transporcie
To niszczenie obiektw lokalnych moe si wydawa tak oczywiste, e nie warto
powica temu a osobnego paragrafu. Jest jednak co na rzeczy: czynno ta moe by
bowiem powodem pewnych problemw, jeeli nie bdziemy jej wiadomi. Jakich
problemw?
Niedozwolone rzucenie wyjtku
Musimy powiedzie sobie o jednej bardzo wanej zasadzie zwizanej z mechanizmem
wyjtkw w C++. Brzmi ona:
Nie naley rzuca nastpnego wyjtku w czasie, gdy kompilator zajmuje si obsug
poprzedniego.
Co to znaczy? Czy nie moemy uywa instrukcji throw w blokach catch?
Wyjtki 455
Ot nie - jest to dozwolone, ale w sumie nie o tym chcemy mwi :) Musimy sobie
powiedzie, co rozumiemy poprzez obsug wyjtku dokonywan przez kompilator.
Dla nas obsug wyjtku jest kod w bloku catch. Aby jednak mg on by wykonany,
obiekt wyjtku oraz punkt sterowania programu musz tam trafi. Tym zajmuje si
kompilator - to jest wanie jego obsuga wyjtku: dostarczenie go do bloku catch.
Dalej nic go ju nie obchodzi: kod z bloku catch jest traktowany jako normalne
instrukcje, bowiem sam kompilator uznaje ju, e z chwil rozpoczcia ich wykonywania
jego praca zostaa wykonana. Wyjtek zosta przyniesiony i to si liczy.
Tak wic:
Obsuga wyjtku dokonywana przez kompilator polega na jego dostarczeniu go
do odpowiedniego bloku catch przy jednoczesnym odwiniciu stosu.
Teraz ju wiemy, na czym polega zastrzeenie podane na pocztku. Nie moemy rzuci
nastpnego wyjtku w chwili, gdy kompilator zajmuje si jeszcze transportem
poprzedniego. Inaczej mwic, midzy wykonaniem instrukcji throw a obsug wyjtku w
bloku catch nie moe wystapi nastpna instrukcja throw.
Strefy bezwyjtkowe
No dobrze, ale waciwie co z tego? Przecie po rzuceniu jednego wyjtku wszystkim
zajmuje si ju kompilator. Jak wic moglibymy rzuci kolejny wyjtek, zanim ten
pierwszy dotrze do bloku catch?
Faktycznie, tak mogoby si wydawa. W rzeczywistoci istniej a dwa miejsca, z
ktrych mona rzuci drugi wyjtek.
Jeli chodzi o pierwsze, to pewnie si go domylasz, jeeli uwanie czytae opis procesu
odwijania stosu i zwizanego z nim niszczenia obiektw lokalnych. Powiedziaem tam, e
przebiega ono w identyczny sposb, jak normalnie. Pami jest zawsze zwalniania, a w
przypadku obiektw klas wywoywane s destruktory.
Bingo! Destruktory s wanie tymi procedurami, ktre s wywoywane podczas obsugi
wyjtku dokonywanej przez kompilator. A zatem nie moemy wyrzuca z nich adnych
wyjtkw, poniewa moe zdarzy, e dany destruktor jest wywoywany podczas
odwijania stosu.
Nie rzucaj wyjtkw z destruktorw.
Druga sytuacja jest bardziej specyficzna. Wiemy, e mechanizm wyjtkw pozwala na
rzucanie obiektw dowolnego typu. Nale do nich take obiekty klas, ktre sami sobie
zdefiniujemy. Definiowanie takich specjalnych klas wyjtkw to zreszt bardzo podana
i rozsdna praktyka. Pomwimy sobie jeszcze o niej.
Jednak niezalenie od tego, jakiego rodzaju obiekty rzucamy, kompilator z kadym
postpuje tak samo. Podczas transportu wyjtku do catch czyni on przynajmniej jedn
kopi obiektu rzucanego. W przypadku typw podstawowych nie jest to aden problem,
ale dla klas wykorzystywane s normalne sposoby ich kopiowania. Znaczy to, e moe
zosta uyty konstruktor kopiujcy - nasz wasny.
Mamy wic drugie (i na szczcie ostatnie) potencjalne miejsce, skd mona rzuci nowy
wyjtek w trakcie obsugi starego. Pamitajmy wic o ostrzeeniu:
Nie rzucajmy nowych wyjtkw z konstruktorw kopiujcych klas, ktrych obiekty
rzucamy jako wyjtki.
Z tych dwch miejsc (wszystkie destruktory i konstruktory kopiujce obiektw
rzucanych) nie powinnimy rzuca adnych wyjtkw. W przeciwnym wypadku
kompilator uzna to za bardzo powany bd. Zaraz si przekonamy, jak powany
Zaawansowane C++ 456
Biblioteka Standardowa udostpnia prost funkcj uncaught_exception(). Zwraca ona
true, jeeli kompilator jest w trakcie obsugi wyjtku. Mona jej uy, jeli koniecznie
musimy rzuci wyjtek w destruktorze; oczywicie powinnimy to zrobi tylko wtedy, gdy
funkcja zwrci false.
Prototyp tej funkcji znajduje si w pliku nagwkowym exception w przestrzeni nazw std.
Skutki wypadku
Co si stanie, jeeli zignorujemy ktry z zakazw podanych wyej i rzucimy nowy
wyjtek w trakcie obsugi innego?
Bdzie to wtedy bardzo powana sytuacja. Oznacza ona bdzie, e kompilator nie jest w
stanie poprawnie przeprowadzi obsugi wyjtku. Inaczej mwic, mechanizm
wyjtkw zawiedzie - tyle e bdzie to rzecz jasna nasza wina.
Co moe wwczas zrobi kompilator? Niewiele. Jedyne, co wtedy czyni, to wywoanie
funkcji terminate(). Skutkiem jest wic nieprzewidziane zakoczenie programu.
Naturalnie, zmiana funkcji terminate() (poprzez set_terminate()) sprawi, e zamiast
domylnej bdzie wywoywana nasza procedura. Piszc j powinnimy pamita, e
funkcja terminate() jest wywoywana w dwch sytuacjach:
gdy wyjtek nie zosta zapany przez aden blok catch
gdy zosta rzucony nowy wyjtek w trakcie obsugi poprzedniego
Obie s sytuacjami krytycznymi. Zatem niezalenie od tego, jakie dodatkowe akcje
bdziemy podejmowa w naszej funkcji, zawsze musimy na koniec zamkn nasz
program. W aplikacjach konsolowych mona uczyni to poprzez exit().
Zarzdzanie zasobami w obliczu wyjtkw
Napisaem wczeniej, e transport rzuconego wyjtku do bloku catch powoduje
zniszczenie wszystkich obiektw lokalnych znajdujcych si po drodze. Nie musimy si
o to martwi; zreszt, nie troszczylimy si o nie take i wtedy, gdy nie korzystalimy z
wyjtkw.
Obiekty lokalne nie s jednak jedynymi z jakich korzystamy w C++. Wiemy te, e
moliwe jest dynamiczne tworzenie obiektw na stercie, czyli w rezerwuarze pamici.
Dokonujemy tego poprzez new.
Pami jest z kolei jednym z tak zwanych zasobw (ang. resources), czyli zewntrznych
bogactw naturalnych komputera. Moemy do nich zaliczy nie tylko pami operacyjn,
ale np. otwarte pliki dyskowe, wyczno na wykorzystanie pewnych urzdze lub
aktywne poczenia internetowe. Waciwe korzystanie z takich zasobw jest jednym z
zada kadego powanego programu.
Zazwyczaj odbywa si ono wedug prostego schematu:
najpierw pozyskujemy dany zasb w jaki sposb (np. alokujemy pami
poprzez new)
potem moemy do woli korzysta z tego zasobu (np. zapisywac dane do pamici)
na koniec zwalniamy zasb, jeeli nie jest ju nam potrzebny (czyli korzystamy z
delete w przypadku pamici)
Najbardziej znany nam zasob, czyli pami opercyjna, jest przez nas wykorzystywany
choby tak:
CFoo* pFoo = new CFoo; // alokacja (utworzenie) obiektu-zasobu
// (robimy co...)
Wyjtki 457
delete pFoo; // zwolnienie obiektu-zasobu
Midzy stworzeniem a zniszczeniem obiektu moe jednak zaj sporo zdarze. W
szczeglnoci: moliwe jest rzucenie wyjtku.
Co si wtedy stanie? Wydawa by si mogo, e obiekt zostanie zniszczony, bo przecie
tak byo zawsze Bd! Obiekt, na ktry wskazuje pFoo nie zostanie zwolniony z
prostego powodu: nie jest on obiektem lokalnym, rezydujcym na stosie, lecz tworzonym
dynamicznie na stercie. Sami wydajemy polecenie jego utworzenia (new), wic rwnie
sami musimy go potem usun (poprzez delete). Zostanie natomiast zniszczony
wskanik na niego (zmienna pFoo), bo jest to zmienna lokalna - co aczkolwiek nie jest
dla nas adn korzyci.
Moesz zapyta: A w czym problem? Skoro pami naley zwolni, to zrbmy to przed
rzuceniem wyjtku - o tak:
try
{
CFoo* pFoo = new CFoo;
// ...
if (warunek_rzucenia_wyjtku)
{
delete pFoo;
throw wyjtek;
}
// ...
delete pFoo;
}
catch (typ obiekt)
{
// ...
}
To powinno rozwiza problem.
Taki sposb to jednak oznaka skrajnego i niestety nieuzasadnionego optymizmu. Bo kto
nam zagwarantuje, e wyjtki, ktre mog nam przeszkadza, bd rzucane wycznie
przez nas? Moemy przecie wywoa jak zewntrzn funkcj, ktra sama bdzie
wyrzucaa wyjtki - nie pytajc nas o zgod i nie baczc na nasz zaalokowan pami, o
ktrej przecie nic nie wie!
To te nie katastrofa, odpowiesz, Moemy przecie wykry rzucenie wyjtku i w
odpowiedzi zwolni pami:
try
{
CFoo* pFoo = new CFoo;
// ...
try
{
// wywoanie funkcji potencjalnie rzucajcej wyjtki
FunkcjaKtoraMozeWyrzucicWyjatek();
}
catch (...)
{
// niszczymy obiekt
delete pFoo;
// rzucamy dalej otrzymany wyjtek
Zaawansowane C++ 458
throw;
}
// ...
delete pFoo;
}
catch (typ obiekt)
{
// ...
}
Blok catch(...) zapie nam wszystkie wyjtki, a my w jego wntrzu zwolnimy pami i
rzucimy je dalej poprzez throw;. Wszystko proste, czy nie?
Brawo, twoja pomysowo jest cakiem dua. Ju widz te dziesitki wywoa funkcji
bibliotecznych, zamknitych w ich wasne bloki try-catch(...), ktre dbaj o zwalnianie
pamici Jak sdzisz, na ile eleganckie, efektywne (zarwno pod wzgldem czasu
wykonania jak i zakodowania) i atwe w konserwacji jest takie rozwizanie?
Jeeli zastanowisz si nad tym cho troch dusz chwil, to zauwaysz, e to bardzo ze
wyjcie. Jego stosowanie (podobnie zreszt jak delete przed throw) jest wiadectwem
koszmarnego stylu programowania. Pomylmy tylko, e wymaga to wielokrotnego
napisania instrukcji delete - powoduje to, e kod staje si bardzo nieczytelny: na
pierwszy rzut oka mona pomyle, e kilka(nacie) razy usuwany jest obiekt, ktry
tworzymy tylko raz. Poza tym obecno tego samego kodu w wielu miejscach znakomicie
utrudnia jego zmian.
By moe teraz pomylae o preprocesorze i jego makrach Jeli naprawd chciaby go
zastosowa, to bardzo prosz. Potem jednak nie narzekaj, e wyprodukowae kod, ktry
stanowi zagadk dla jasnowidza.
Teraz moesz si oburzy: No to co naley zrobi?! Przecie nie moemy dopuci do
powstawania wyciekw pamici czy niezamykania plikw! Moe naley po prostu
zrezygnowa z tak nieprzyjaznego narzdzia, jak wyjtki? C, moemy nie lubi
wyjtkw (szczeglnie w tej chwili), ale nigdy od nich nie uciekniemy. Jeeli sami nie
bdziemy ich stosowa, to uyje ich kto inny, ktrego kodu my bdziemy potrzebowali.
Na wyjtki nie powinnimy si wic obraa, lecz sprbowa je zrozumie. Rozwizanie
problemu zasobw, ktre zaproponowalimy wyej, jest ze, poniewa prbuje wtrci si
w automatyczny proces odwijania stosu ze swoim rcznym zwalnianiem zasobw (tutaj
pamici). Nie tdy droga; naley raczej zastosowa tak metod, ktra pozwoli nam
czerpa korzyci z automatyki wyjtkw.
Teraz poznamy waciwy sposb dokonania tego.
Problem z niezwolnionymi zasobami wystpuje we wszystkich jzykach, w ktrych
funkcjonuj wyjtki. Trzeba jednak przyzna, e w wikszoci z nich poradzono sobie z
nim znacznie lepiej ni w C++. Przykadowo, Java i Object Pascal posiadaj moliwo
zdefiniowania dodatkowego (obok catch) bloku finally (nareszcie). W nim zostaje
umieszczany kod wykonywany zawsze - niezalenie od tego, czy wyjtek w try wystpi,
czy te nie. Jest to wic idealne miejsce na instrukcje zwalniajce zasoby, pozyskane w
bloku try. Mamy bowiem gwarancj, i zostan one poprawnie oddane niezalenie od
okolicznoci.
Opakowywanie
Pomys jest do prosty. Jak wiemy, podczas odwijania stosu niszczone s wszystkie
obiekty lokalne. W przypadku, gdy s to obiekty naszych wasnych klas, do pracy ruszaj
wtedy destruktory tych klas. Wanie we wntrzu tych destruktorw moemy umieci
kod zwalniajcy przydzielon pami czy jakikolwiek inny zasb.
Wyjtki 459
Wydaje si to podobne do rcznego zwalniania zasobw przed rzuceniem wyjtku lub w
blokach catch(...). Jest jednak jedna bardzo wana rnica: nie musimy tutaj
wiedzie, w ktrym dokadnie miejscu wystpi wyjtek. Kompilator bowiem i tak wywoa
destruktor obiektu - niewane, gdzie i jaki wyjtek zosta rzucony.
Skoro jednak mamy uywa destruktorw, to trzeba rzecz jasna zdefiniowa jakie klasy.
Potem za naley w bloku try tworzy obiekty tyche klas, by ich destruktory zostay
wywoane w przypadku wyrzucenia jakiego wyjtku.
Jak to naley uczyni? Kwestia nie jest trudna. Najlepiej jest zrobi tak, aby dla kadego
pojedynczego zasobu (jak zaalokawany blok pamici, otwarty plik, itp.) istnia jeden
obiekt. W momencie zniszczenia tego obiektu (z powodu rzucenia wyjtku) zostanie
wywoany destruktor jego klasy, ktry zwolni zasb (czyli np. usunie pami albo
zamknie plik).
Destruktor wskanika?
To bardzo proste, prawda? ;) Ale eby byo jeszcze atwiejsze, spjrzmy na prosty
przykad. Zajmiemy si zasobem, ktry najbardziej znamy, czyli pamici operacyjn;
oto przykad kodu, ktry moe spowodowa jej wyciek:
try
{
CFoo* pFoo = new CFoo;
// ...
throw "Cos sie stalo";
// obiekt niezwolniony, mamy wyciek!
}
// (tutaj catch)
Przyczyna jest oczywicie taka, i odwijanie stosu nie usunie obiektu zaalokowanego
dynamicznie na stercie. Usunity zostanie rzecz jasna sam wskanik (czyli zmienna
pFoo), ale na tym si skoczy. Kompilator nie zajmie si obiektem, na ktry w wskanik
pokazuje.
Zapytasz: A czemu nie? Przecie mgby to zrobi. Pomyl jednak, e nie musi to by
wcale jedyny wskanik pokazujcy na dynamiczny obiekt. W przypadku usunicia obiektu
wszystkie pozostae stayby si niewane. Oprcz tego byoby to zamanie zasady, i
obiekty stworzone jawnie (poprzez new) musz by take jawnie zniszczone (przez
delete).
My jednak chcielibymy, aby wraz z kocem ycia wskanika skoczy si take ywot
pamici, na ktr on pokazuje. Jak mona to osign?
C, gdyby nasz wskanik by obiektem jakiej klasy, wtedy moglibymy napisa
instrukcj delete w jej destruktorze. Tak jest jednak nie jest: wskanik to typ
wbudowany
118
, wic nie moemy napisa dla destruktora - podobnie jak nie moemy
tego zrobi dla typu int czy float.
Sprytny wskanik
Wskanik musiaby wic by klas Dlaczego nie? Podkrelaem w zeszym rozdziale, e
klasy w C++ s tak pomylane, aby mogy one naladowa typy podstawowe. Czemu
zatem nie monaby stworzy sobie takiej klasy, ktra dziaaby jak wskanik - typ
118
Wskanik moe wprawdzie pokazywa na typ zdefiniowany przez uytkownika, ale sam zawsze bdzie typem
wbudowanym. Jest to przecie zwyka liczba - adres w pamici.
Zaawansowane C++ 460
wbudowany? Wtedy mielibymy pen swobod w okreleniu jej destruktora, a take
innych metod.
Oczywicie, nie my pierwsi wpadlimy na ten pomys. To rozwizanie jest szeroko znane i
nosi nazw sprytnych wskanikw (ang. smart pointers). Takie wskaniki s podobne
do zwykych, jednak przy okazji oddaj jeszcze pewne dodatkowe przysugi. W naszym
przypadku chodzi o dbao o zwolnienie pamici w przypadku wystpienia wyjtku.
Sprytny wskanik jest klas. Ma ona jednak odpowiednio przecione operatory - tak, e
korzystanie z jej obiektw niczym nie rni si od korzystania z normalnych wskanikw.
Popatrzmy na znany z zeszego rozdziau przykad:
class CFooSmartPtr
{
private:
// opakowywany, waciwy wskanik
CFoo* m_pWskaznik;
public:
// konstruktor i destruktor
CFooSmartPtr(CFoo* pFoo) : m_pWskaznik(pFoo) { }
~CFooSmartPtr() { if (m_pWskaznik) delete m_pWskaznik; }
//-------------------------------------------------------------
// operator dereferencji
CFoo& operator*() { return *m_pWskaznik; }
// operator wyuskania
CFoo* operator->() { return m_pWskaznik; }
};
Jest to inteligentny wskanik na obiekty klasy CFoo; docelowy typ jest jednak nieistotny,
bo rwnie dobrze monaby pokazywa na liczby typu int czy te inne obiekty. Wana
jest zasada dziaania - zupenie nieskomplikowana.
Klasy CFooSmartPtr uywamy po prostu zamiast typu CFoo*:
try
{
CFooSmartPtr pFoo = new CFoo;
// ...
throw "Cos sie stalo";
// niszczony obiekt pFoo i wywoywany destruktor CFooSmartPtr
}
// (tutaj catch)
Dziki przecieniu operatorw korzystamy ze sprytnego wskanika dokadnie w ten sam
sposb, jak ze zwykego. Poza tym rozwizujemy problem ze zwolnieniem pamici:
zajmuje si tym destruktor klasy CFooSmartPtr. Stosuje on operator delete wobec
waciwego, wewntrznego wskanika (typu normalnego, czyli CFoo*), usuwajc
stworzony dynamicznie obiekt. Robi niezalenie od tego, gdzie i kiedy (i czy) wystpi
jakikolwiek wyjtek. Wystarczy, e zostanie zlikwidowany obiekt pFoo, a to pocignie za
sob zwolnienie pamici.
I o to nam wanie chodzio. Wykorzystalimy mechanizm odwijania stosu do zwolnienia
zasobw, ktre normalnie byyby pozostawione same sobie. Nasz problem zosta
rozwizany.
Wyjtki 461
Nieco uwag
Aby jednak nie byo a tak bardzo piknie, na koniec paragrafu musz jeszcze troch
pogldzi :) Chodzi mianowicie o dwie wane sprawy zwizane ze sprytnymi
wskanikami, ktrych uywamy w poczeniu z mechanizmem wyjtkw.
Rne typy wskanikw
Zaprezentowana wyej klasa CFooSmartPtr jest typem inteligentnego wskanika, ktry
moe pokazywa na obiekty jakiej zdefiniowanej wczeniej klasy CFoo. Przy jego
pomocy nie moemy odnosi si do obiektw innych klas czy typw podstawowych.
Jeli jednak bdzie to konieczne, wwczas musimy niestety napisa now klas
wskanika. Nie jest to trudne: wystarczy w definicji CFooSmartPtr zmieni wystpienia
CFoo np. na int. W nastpnym rozdziale poznamy zreszt o wiele bardziej efektywn
technik (mianowicie szablony), ktra uwolni nas od tej mudnej pracy.
Za chwil te przyjrzymy si rozwizaniu, jakie przygotowali dla nas sami twrcy C++ w
Bibliotece Standardowej.
Uywajmy tylko tam, gdzie to konieczne
Musz te powtrzy to, o czym ju wspomniaem przy pierwszym spotkaniu ze
sprytnymi wskanikami. Ot trzeba pamita, e nie s one uniwersalnym lekiem na
wszystkie bolczki programisty. Nie naley ich stosowa wszdzie, poniewa kady rodzaj
inteligentnego wskanika (my na razie poznalimy jeden) ma cile okrelone
zastosowania.
W sytuacjach, w ktrych z powodzeniem sprawdzaj si zwyke wskaniki, powinnimy
nadal z nich korzysta. Dopiero w takich przypadkach, gdy s one niewystarczajce,
musimy sign go bardziej wyrafinowane rozwizania. Takim przypadkiem jest wanie
rzucanie wyjtkw.
Co ju zrobiono za nas
Metoda opakowywania zasobw moe si wydawa nazbyt praco- i czasochonna, a
przede wszystkim wtrna. Stosujc j pewnie szybko zauwayby, e napisane przez
ciebie klasy powinny by obecne w niemal kadym programie korzystajcym z wyjtkw.
Naturalnie, mog by one dobrym punktem wyjcia dla twojej wasnej biblioteki z
przydatnymi kodami, uywanymi w wielu aplikacjach. Niewykluczone, e kiedy bdziesz
musia napisa przynajmniej kilka takich klas-opakowa, jeeli zechcesz skorzysta z
zasobw innych ni pami operacyjna czy pliki dyskowe.
Na razie jednak lepiej chyba sprawdz si narzdzia, ktre otrzymujesz wraz z jzykiem
C++ i jego Bibliotek Standardow. Zobaczmy pokrtce, jak one dziaaj; ich dokadny
opis znajdziesz w kolejnych rozdziaach, powiconych samej tylko Bibliotece
Standardowej.
Klasa std::auto_ptr
Sprytne wskaniki chronice przed wyciekami pamici, powstajcymi przy rzucaniu
wyjtkw, s do czsto uywane w praktyce. Samodzielne ich definiowanie byoby wic
uciliwe. W C++ mamy wic ju stworzon do tego klas std::auto_ptr.
cilej mwic, auto_ptr jest szablonem klasy. Co to dokadnie znaczy, dowiesz si w
nastpnym rozdziale. Pki co bdziesz wiedzia, i pozwala to na uywanie auto_ptr w
charakterze wskanika do dowolnego typu danych. Nie musimy ju zatem definiowia
adnych klas.
Aby skorzysta z auto_ptr, trzeba jedynie doczy standardowy plik nagwkowy
memory:
Zaawansowane C++ 462
#include <memory>
Teraz moemy ju korzysta z tego narzdzia. Z powodzeniem moe ono zastpi nasz
pieczoowicie wypracowan klas CFooSmartPtr:
try
{
std::auto_ptr<CFoo> pFoo(new CFoo);
// ...
throw "Cos sie stalo";
// przy niszczeniu wskanika auto_ptr zwalniana jest pami
}
// (tutaj catch)
Konstrukcja std::auto_ptr<CFoo> pewnie wyglda nieco dziwnie, ale atwo si do niej
przyzwyczaisz, gdy ju poznasz szablony. Mona z niej take wydedukowa, e w
nawiasach ktowych <> podajemy typ danych, na ktry chcemy pokazywa poprzez
auto_ptr - tutaj jest to CFoo. atwo domyli si, e chcc mie wskanik na typ int,
piszemy std::auto_ptr<int>, itp.
Zwrmy jeszcze uwag, w jaki sposob umieszcza si instrukcj new w deklaracji
wskanika. Z pewnych powodw, o ktrych nie warto tu mwi, konstruktor klasy
auto_ptr jest opatrzony swkiem explicit. Dlatego te nie mona uy znaku =, lecz
trzeba jawnie przekaza parametr, bdcy normalnym wskanikiem do zaalokowanego
poprzez new obszaru pamici.
W sumie wic skadnia deklaracji wskanika auto_ptr wyglda tak:
std::auto_ptr<typ> wskanik(new typ[(parametry_konstruktora_typu)]);
O zwolnienie pamici nie musimy si martwi. Destruktor auto_ptr usunie j zawsze,
niezalenie od tego, czy wyjtek faktycznie wystpi.
Pliki w Bibliotece Standardowej
Oprcz pamici drugim wanym rodzajem zasobw s pliki dyskowe. O dokadnym
sposobie ich obsugi powiemy sobie aczkolwiek dopiero wtedy, gdy zajmiemy si
strumieniami Biblioteki Standardowej.
Tutaj chc tylko wspomnie, e metody dostpu do plikw, jakie s tam oferowane,
cakowicie poprawnie wsppracuj z wyjtkami. Oto przykad:
#include <fstream>
try
{
// stworzenie strumienia i otwarcie pliku do zapisu
std::ofstream Plik("plik.txt", ios::out);
// zapisanie czego do pliku
Plik << "Co";
// ...
throw "Cos sie stalo";
// strumie jest niszczony, a plik zamykany
}
// (tutaj catch)
Wyjtki 463
Plik reprezentowany przez strumie Plik zostanie zawsze zamknity. W kadym
przypadku - wystpienia wyjtku lub nie - wywoany bowiem bdzie destruktor klasy
ofstream, a on tym si wanie zajmie. Nie trzeba wic martwi si o to.
***
Tak zakoczymy omawianie procesu odwijania stosu i jego konsekwencji. Teraz
zobaczysz, jak w praktyce powinno si korzysta z mechanizmu wyjtkw w C++.
Wykorzystanie wyjtkw
Dwa poprzednie podrozdziay mwiy o tym, czym s wyjtki i jak dziaa ten mechanizm
w C++. W zasadzie na tym monaby poprzesta, ale taki opis na pewno nie bdzie
wystarczajcy. Jak kady element jzyka, take i wyjtki naley uywa we waciwy
sposb; korzystaniu z wyjtkw w praktyce zostanie wic powicony ten podrozdzia.
Wyjtki w praktyce
Zanim z pieni na ustach zabierzemy si do wykorzystywania wyjtkw, musimy sobie
odpowiedzie na jedno fundamentalne pytanie: czy tego potrzebujemy? Takie
postawienie sprawy jest pewnie zaskakujce - dotd wszystkie poznawane przez nas
elementy C++ byy waciwie niezbdne do efektywnego stosowania tego jzyka. Czy z
wyjtkami jest inaczej? Przyjrzyjmy si sprawie bliej
Moe powiedzmy sobie o dwch podstawowych sytuacjach, kiedy wyjtkw nie
powinnimy stosowa. W zasadzie mona je zamkn w jedno stwierdzenie:
Nie powinno si wykorzystywa wyjtkw tam, gdzie z powodzeniem wystarczaj inne
techniki sygnalizowania i obsugi bdw.
Oznacza to, e:
nie powinnimy na si dodawa wyjtkw do istniejcego programu. Jeeli po
przetestowaniu dziaa on dobrze i efektywnie bez wyjtkw, nie ma adnego
powodu, aby wprowadza do kodu ten mechanizm
dla tworzonych od nowa, lecz krtkich programw wyjtki mog by zbyt
potnym narzdziem. Wysiek woony w jego zaprogramowanie (jak si zaraz
przekonamy - wcale niemay) nie musi si opaca. Co oznacza pojcie krtki
program, to ju kady musi sobie odpowiedzie sam; zwykle uwaa si, e
krtkie s te aplikacje, ktre nie przekraczaj rozmiarami 1000-2000 linijek kodu
Wida wic, e nie kady program musi koniecznie stosowa ten mechanizm. S
oczywicie sytuacje, gdy oby si bez niego jest bardzo trudno, jednak naduywanie
wyjtkw jest zazwyczaj gorsze ni ich niedostatek. O obu sprawach (korzyciach
pyncych z wyjtkw i ich przesadnemu stosowaniu) powiemy sobie jeszcze pniej.
Zamy jednak, e zdecydowalimy si wykorzystywa wyjtki. Jak poprawnie
zrealizowa te intencje? Jak wikszo rzeczy w programowaniu, nie jest to trudne :)
Musimy mianowicie:
pomyle, jakie sytuacje wyjtkowe mog wystpi w naszej aplikacji i wyrni
wrd nich poszczeglne rodzaje, a nawet pewn hierarchi. To pozwoli na
stworzenie odpowiednich klas dla obiektw wyjtkw, czym zajmiemy si w
pierwszym paragrafie
Zaawansowane C++ 464
we waciwy sposb zorganizowa obsug wyjtkw - chodzi gwnie o
rozmieszczenie blokw try i catch. Ta kwestia bdzie przedmiotem drugiego
paragrafu
Potem moemy ju tylko mie nadziej, e nasza ciko wykonana praca nigdy nie
bdzie potrzebna. Najlepiej przecie byoby, aby sytuacje wyjtkowe nie zdarzay si, a
nasze programy dziaay zawsze zgodnie z zamierzeniami C, praca programisty nie
jest usana rami, wic tak nigdy nie bdzie. Nauczmy si wic poprawnie reagowa na
wszelkiego typu nieprzewidziane zdarzenia, jakie mog si przytrafi naszym aplikacjom.
Projektowanie klas wyjtkw
C++ umoliwia rzucenie w charakterze wyjtkw obiektw dowolnych typw, take tych
wbudowanych. Taka moliwo jest jednak mao pocigajca, jako e pojedyncza liczba
czy napis nie nios zwykle wystarczajcej wiedzy o powstaej sytuacji.
Dlatego te powszechn praktyk jest tworzenie wasnych typw (klas) dla obiektw
wyjtkw. Takie klasy zawieraj w sobie wicej informacji zebranych z miejsca
katastrofy, ktre mog by przydatne w rozpoznaniu i rozwizaniu problemu.
Definiujemy klas
Co wic powinien zawiera taki obiekt? Najwaniejsze jest ustalenie rodzaju bdu oraz
miejsca jego wystpienia w kodzie. Typowym zestawem danych dla wyjtku moe by
zatem:
nazwa pliku z kodem i numer wiersza, w ktrym rzucono wyjtek. Do tego mona
doda jeszcze dat kompilacji programu, aby rozrni jego poszczeglne wersje
dane identyfikacyjne bdu - w najprostszej wersji tekstowy komunikat
Nasza klasa wyjtku mogaby wic wyglda tak:
#include <string>
class CException
{
private:
// dane wyjtku
std::string m_strNazwaPliku;
unsigned m_uLinijka;
std::string m_strKomunikat;
public:
// konstruktor
CException(const std::string& strNazwaPliku,
unsigned uLinijka,
const std::string& strKomunikat)
: m_strNazwaPliku(strNazwaPliku),
m_uLinijka(uLinijka),
m_strKomunikat(strKomunikat) { }
//-------------------------------------------------------------
// metody dostpowe
std::string NazwaPliku() const { return m_strNazwaPliku; }
unsigned Linijka() const { return m_uLinijka; }
std::string Komunikat() const { return m_strKomunikat; }
};
Do obszerny konstruktor pozwala na podanie wszystkich danych za jednym zamachem,
w instrukcji throw:
Wyjtki 465
throw CException(__FILE__, __LINE__, "Cos sie stalo");
Dla wygody mona sobie nawet zdefiniowa odpowiednie makro, jako e __FILE__ i
__LINE__ pojawi si w kadej instrukcji rzucenia wyjtku. Jest to szczeglnie przydatne,
jeeli do wyjtku doczymy jeszcze inne informacje pochodzce predefiniowanych
symboli preprocesora.
Take konstruktor klasy moe dokonywa zbierania jakich informacji od programu.
Mog to by np. zrzuty pamici (ang. memory dumps), czyli obrazy zawartoci
kluczowych miejsc pamici operacyjnej. Takie zaawansowane techniki s aczkolwiek
przydatne tylko w naprawd duych programach.
Po zapaniu takiego obiektu moemy pokaza zwizane z nim dane - na przykad tak:
catch (CException& Wyjatek)
{
std::cout << " Wystapil wyjatek " << std::endl;
std::cout << "---------------------------" << std::endl;
std::cout << "Komunikat:\t" << Wyjatek.Komunikat() << std::endl;
std::cout << "Plik:\t" << Wyjatek.NazwaPliku() << std::endl;
std::cout << "Wiersz kodu:\t" << Wyjatek.Linijka() << std::endl;
}
Jest to ju cakiem zadowalajca informacja o bdzie.
Hierarchia wyjtkw
Pojedyncza klasa wyjtku rzadko jest jednak wystarczajca. Wad takiego skromnego
rozwizania jest to, e ze wzgldu na charakter danych o sytuacji wyjtkowej, jakie
zawiera obiekt, ograniczamy sobie moliwo obsugi wyjtku. W naszym przypadku
trudno jest podj jakiekolwiek dziaania poza wywietleniem komunikatu i zamkniciem
programu.
Dla zwikszenia pola manewru monaby doda do klasy jakie pola typu wyliczeniowego,
okrelajce bliej rodzaj bdu; wwczas w bloku catch pojawiaby si pewnie jaka
instrukcja switch.
Jest aczkolwiek praktyczniejsze i bardziej elastyczne wyjcie: moemy uy
dziedziczenia.
Okazuje si, e rozsdne jest stworzenie hierarchii sytuacji wyjtkw i odpowiadajcej jej
hierarchii klas wyjtkw. Opiera si to na spostrzeeniu, e moliwe bdy moemy
najczciej w pewien sposb sklasyfikowa. Przykadowo, monaby wyrni wyjtki
zwizane z pamici, z plikami dyskowymi i obliczeniami matematycznymi: wrd tych
pierwszych mielibymy np. brak pamici (ang. out of memory) i bd ochrony
(ang. access violation); dostp do pliku moe by niemoliwy chociaby z powodu jego
braku albo nieobecnoci dysku w napdzie; dziaania na liczbach mog wreszcie
doprowadzi do dzielenia przez zero lub wycigania pierwiastka z liczby ujemnej.
Taki ukad, oprcz moliwoci rozrnienia poszczeglnych typw wyjtkw, ma jeszcze
jedn zalet. Mona bowiem dla kadego typu zakodowa specyficzny dla niego sposb
obsugi, stosujc do tego metody wirtualne - np. w ten sposb:
// klasa bazowa
class IException
{
public:
// wywietl informacje o wyjtku
Zaawansowane C++ 466
virtual void Wyswietl();
};
// ----------------------------------------------------------------------
// wyjtek zwizany z pamici
class CMemoryException : public IException
{
public:
// dziaania specyficzne dla tego rodzaju wyjtku
virtual void Wyswietl();
};
// wyjtek zwizany z plikami
class CFilesException : public IException
{
public:
// dziaania specyficzne dla tego rodzaju wyjtku
virtual void Wyswietl();
};
Pamitajmy jednak, e nadmierne rozbudowywanie hierarchii te nie ma zbytniego
sensu. Nie wydaje si na przykad suszne wyrnianie osobnych klas dla wyjtkw
dzielenia przez zero, pierwiastka kwadratowego z liczy ujemnej oraz podniesienia zera do
potgi zerowej. Jest bowiem wielce prawdopodobne, e jedyna rnica midzy tymi
sytuacjami bdzie polegaa na treci wywietlanego komunikatu. W takich przypadkach
zdecydowanie wystarczy pojedyncza klasa.
Organizacja obsugi wyjtkw
Zdefiniowana uprzednio klas lub jej hierarchi bdziemy pewnie mieli okazj nieraz
wykorzysta. Poniewa nie jest to takie oczywiste, warto powici temu zagadnieniu
osobny paragraf.
Umiejscowienie blokw try i catch
Wydawaoby si, e obsuga wyjtkw to bardzo prosta czynno - szczeglnie, jeli
mamy ju zdefiniowany dla nich odpowiednie klasy. Niestety, polega to na czym wicej
ni tylko napisaniu niepewnego kodu w bloku try i instrukcji obsugi bdw catch.
Kod warstwowy
Jednym z podstawowych powodw, dla ktrych wprowadzono wyjtki w C++, bya
konieczno zapewnienia jakiego sensownego sposobu reakcji na bdy w programach o
skomplikowanym kodzie. Kady wikszy (i dobrze napisany) program ma bowiem
skonno do rozwarstwiania kodu.
Nie jest to bynajmniej niepodane zjawisko, wrcz przeciwnie. Polega ono na tym, e w
aplikacji moemy wyrni fragmenty wyszego i niszczego poziomu. Te pierwsze
odpowiadaj za ca logik aplikacji, w tym za jej komunikacj z uytkownikiem; te
drugie wykonuj bardziej wewntrzne czynnoci, takie jak na przykad zarzdzanie
pamici operacyjn czy dostp do plikw na dysku.
Taki podzia jest korzystny, poniewa uatwia konserwacj programu, a take
wykorzystywanie pewnych fragmentw kodu (zwaszcza tych niskopoziomowych) w
kolejnych projektach. Funkcje odpowiedzialne za pewne proste czynnoci, jak
wspomniany dostp do plikw nie musz nic wiedzie o tym, kto je wywouje - waciwie
to nawet nie powinny. Innymi sowy:
Kod niszego poziomu powinien by zazwyczaj niezaleny od kodu wyszego poziomu.
Wyjtki 467
Tylko wtedy zachowujemy wymienione wyej zalety warstwowoci programu.
Podawanie bdw wyej
Podzia warstwowy wymusza poza tym do cile ustalony przepyw danych w aplikacji.
Odbywa si on zawsze tak, e kod wyszego poziomu przekazuje do niszych warstw
konieczne informacje (np. nazw pliku, ktry ma by otwarty) i odbiera rezultaty
wykonanych operacji (czyli zawarto pliku). Potem wykorzystuje je do swych wasnych
zada (np. do wywietlenia pliku na ekranie).
Ten naturalny ukad dziaa dobrze dopki si nie zepsuje :) Przyczyn mog by
sytuacje wyjtkowe wystpujce w kodzie niszego poziomu. Typowym przykadem moe
by brak danego pliku, wobec czego jego otwarcie nie jest moliwe. Funkcja, ktra
miaa tego dokona, nie bdzie potrafia poradzi sobie z tym bdem, poniewa nazwa
pliku do otwarcie pochodzia z zewntrz - z gry. Moe jedynie poinformowa
wywoujcego o zainstniaej sytuacji.
I tutaj wkraczaj na scen opisane na samym pocztku rozdziau mechanizmy obsugi
bdw. Jednym z nich s wanie wyjtki.
Dobre wyporodkowanie
Ich stosowanie jest szczeglnie wskazane wanie wtedy, gdy nasz kod ma kilka
logicznych warstw, co zreszt powinno zdarza si jak najczciej. Wwczas odnosimy
jedn zasadnicz korzy: nie musimy martwi si o sposb, w jaki informacja o bdzie
dotrze z pokadw gbinowych programu, gdzie wystpia, na grne pitra, gdzie
mogaby zosta waciwie obsuona.
Naszym problemem jest jednak co innego. O ile zazwyczaj dokadnie wiadomo, gdzie
wyjtek naley rzuci (wiadomo - tam gdzie co si nie powiodo), o tyle trudno moe
sprawi wybranie waciwego miejsca na jego zapanie:
jeeli bdzie ono za nisko, wtedy najprawdopodobniej nie bdzie moliwe
podjcie adnych rozsdnych dziaa w reakcji na wyjtek. Przykadowo,
wymieniona funkcja otwierajca plik nie powinna sama apa wyjtku, ktry rzuci,
bo bdzie wobec niego bezradna. Skoro przecie rzucia ten wyjtek, jest to
wanie znak, i nie radzi sobie z powstaa sytuacj i oddaje inicjatyw komu
bardziej kompetentnemu
z drugiej strony, umieszczenie blokw catch za wysoko powoduje zbyt due
zamieszanie w funkcjonowaniu programu. Powoduje to, e punkt wykonania
przeskakuje o cae kilometry, niespodziewanie przerywajc wszystko znajdujce
si po drodze zdania. Nie naley bowiem zapomina, e po rzuceniu wyjtku nie
ma ju powrotu - dalsze wykonywanie zostanie co najwyej podjte po
wykonaniu bloku catch, ktry ten wyjtek. Cakowitym absurdem jest wic np.
ujcie caej zawartoci funkcji main() w blok try i obsuga wszystkich wyjtkw w
nastpujcym dalej bloku catch. Nietrudno przecie domyli si, e takie
rozwizanie spowoduje zakoczenie programu po kadym wystpieniu wyjtku
Pytanie brzmi wic: jak osign rozsdny kompromis? Trzeba pogodzi ze sob dwie
racje:
konieczno sensownej obsugi wyjtku
konieczno przywrcenia programu do normalnego stanu
Naley wic apa wyjtek w takim miejscu, w ktrym ju moliwe jest jego
obsuenie, ale jednoczenie po jego zakoczeniu program powinien nadal mc podja
podj w miar normaln prac.
Przykad? Jeeli uytkownik wybierze opcj otwarcia pliku, ale potem poda nieistniejc
nazw, program powinien po prostu poinformowa o tym i ponownie zapyta o nazw
Zaawansowane C++ 468
pliku. Nie moe natomiast zmusza uytkownika do ponownego wybrania opcji otwarcia
pliku. A ju na pewno nie moe niespodziewanie koczy swojej pracy - to byoby wrcz
skandaliczne.
Chwytanie wyjtkw w blokach catch
Poprawne chwytanie wyjtkw w blokach catch to kolejne (ostatnie ju na szczcie)
zagadnienie, o ktrym musimy pamita. Wiesz na ten temat ju cakiem sporo, ale
nigdy nie zaszkodzi powtrzy sobie przyswojone wiadomoci i przyswoi nowe.
Szczegy przodem - druga odsona
Swego czasu zwrciem ci uwag na wan spraw kolejnoci blokw catch.
Uwiadomiem, e ich dziaanie tylko z pozoru przypomina przecione funkcje, jako e
porzdek dopasowywania obiektu wyjtku cile pokrywa si z porzdkiem samych
blokw catch, a same dopasowywanie koczy przy pierwszym sukcesie.
W zwizku naley tak ustawia bloki catch, aby na pocztek szy te, ktre precyzyjniej
opisuj typ wyjtku. Gdy zdefiniujemy sobie hierarchi klas wyjtkw, ta zasada zyskuje
jeszcze pewniejsz podstaw. W przypadku typw podstawowych (int, double) moe
by do trudne wyobraenie si relacji typ oglny - typ szczegowy. Natomiast dla
klas jest to oczywiste: wchodzi tu bowiem w gr jednoznaczny zwizek dziedziczenia.
Jakie s wic konkretne wnioski? Ano takie, e:
Gdy stosujemy hierarchi klas wyjtkw, powinnimy najpierw prbowa apa
obiekty klas pochodnych, a dopiero potem obiekty klas bazowych.
Mam nadziej, i wiesz doskonale, z jakiej fundamentalnej reguy programowania
obiektowego wynika powysza zasada
119
.
Jeeli zastosujemy klasy wyjtkw z poprzedniego paragrafu, to ilustracj moe by taki
kawaek kodu:
try
{
// ...
}
catch (CMemoryException& Wyjatek)
{
// ...
}
catch (CFilesException& Wyjatek)
{
// ...
}
catch (IException& Wyjatek)
{
// ...
}
Instrukcje chwytajce bardziej wyspecjalizowane wyjtki - CMemoryException i
CFilesException - umieszczamy na samej grze. Dopiero niej zajmujemy si
pozostaymi wyjtkami, chwytajc obiekty typu bazowego IException. Gdybymy czynili
to na pocztku, zapalibymy absolutnie wszystkie swoje wyjtki - nie dajc sobie szansy
na rozrnienie bdw pamici od wyjtkw plikowych lub innych.
119
Oczywicie wynika ona std, e obiekt klasy pochodnej jest jednoczenie obiektem klasy bazowej. Albo te
std, e zawsze istnieje niejawna konwersja z klasy pochodnej na klasy bazowej - jakkolwiek to wyrazimy,
bdzie poprawnie.
Wyjtki 469
Wida wic po raz kolejny, e waciwe uporzdkowanie blokw catch ma niebagatelne
znaczenie.
Lepiej referencj
We wszystkich przytoczonych ostatnio kodach apaem wyjatki poprzez referencje do
nich, a nie poprzez same obiekty. Zbywalimy to dotd milczeniem, ale czas ten fakt
wyjani.
Przyczyna jest waciwie cakiem prosta. Referencje s, jak pamitamy,
zakamuflowanymi wskanikami: faktycznie rni si od wskanikw tylko drobnymi
szczegami, jak choby skadni. Zachowuj jednak ich jedn cenn waciwo
obiektow: pozwalaj na stosowanie polimorfizmu metod wirtualnych.
To doskonalne znane nam zjawisko jest wic moliwe do wykorzystania take przy
obsudze wyjtkw. Oto przykad:
try
{
// ...
}
catch (IException& Wyjatek)
{
// wywoanie metody wirtualnej, pno wizanej
Wyjatek.Wyswietl();
}
Metoda wirtualna Wyswietl() jest tu pno wizana, zatem to, ktry jej wariant - z klasy
podstawowej czy pochodnej - zostanie wywoany, decyduje si podczas dziaania
programu. Jest to wic inny sposb na swoiste rozrnienie typu wyjtku i podjcie
dziaa celem jego obsugi.
Uwagi oglne
Na sam koniec podziel si jeszcze garci uwag oglnych dotyczcych wyjtkw. Przede
wszystkim zastanowimy si nad korzyciami z uywania tego mechanizmu oraz
sytuacjami, gdzie czsto jest on naduywany.
Korzyci ze stosowania wyjtkw
Podstawowe zalety wyjtkw przedstawiem na pocztku rozdziau, gdy porwnywaem je
z innymi sposobami obsugi bdw. Teraz jednak masz ju za sob dogbne poznanie
tej techniki, wic pewnie zwtpie w te przymioty ;) Nawet jeli nie, to pokazane niej
argumenty przemawiajce na korzy wyjtkw mog pomc ci w decyzji co do ich
wykorzystania w konkretnej sytuacji.
Informacja o bdzie w kadej sytuacji
Pierwsz przewag, jak wyjtki maj nad innymi sposobami sygnalizowania bdw, jest
uniwersalno: moemy je bowiem stosowa w kadej sytuacji i w kadej funkcji.
No ale czy to co nadzwyczajnego? Przecie wydawaoby si, e zarwno technika
zwracania kodu bdu jak i wywoanie zwrotne, moe by zastosowane wszdzie. To
jednak nieprawda; oba te sposoby wymagaj odpowiedniej deklaracji funkcji,
uwzgldniajcej ich wykorzystanie. A nagwek funkcji moe by czsto ograniczony
przez sam jzyk albo inne czynniki - jest tak na przykad w:
konstruktorach
wikszoci przecionych operatorw
funkcjach zwrotnych dla zewntrznych bibliotek
Zaawansowane C++ 470
Do tej grupy monaby prbowa zaliczy te destruktory, ale jak przecie, z destruktorw
nie mona rzuca wyjtkw.
Dziki temu, e wyjtki nie opieraj si na normalnym sposobie wywoywania i powrotu z
funkcji, mog by uywane take i w tych specjalnych funkcjach.
Uproszczenie kodu
Jakkolwiek dziwnie to zabrzmi, wyjtki umoliwiaj te znaczne uproszczenie kodu i
uczynienie go przejrzystszym. Jest tak, gdy pozwalaj one przenie sekwencje
odpowiedzialne za obsug bdw do osobnych blokw, z dala od waciwych instrukcji.
W normalnym kodzie procedury wygldaj mniej wicej tak:
zrb co
sprawd, czy si udao
zrb co innego
sprawd, czy si udao
zrb jeszcze co
sprawd, czy nie byo bdw
itd.
Wyrnione tu sprawdzenia bdw s realizowane zwykle przy pomocy instrukcji if lub
switch. Przy ich uyciu kod staje si wic pltanin instrukcji warunkowych, raczej
trudnych do czytania.
Gdy za uywamy wyjtkw, to obsuga bdw przenosi si na koniec algorytmu:
zrb co
zrb co innego
zrb jeszcze co
itd.
obsu ewentualne niepowodzenia
Oczywicie dla tych, ktrzy nie dbaj o porzdek w kodzie, jest to aden argument, ale ty
si chyba do nich nie zaliczasz?
Wzrost niezawodnoci kodu
Wreszcie mona wytoczy najcisze dziaa. Wyjtki nie pozwalaj na obojtno - na
ignorowanie bdw.
Poprzedni akapit uwiadamia, e tradycyjne metody w rodzaju zwracania rezultatu musz
by aktywnie wspomagane przez programist, ktry uywa wykorzystujcych je funkcji.
Nie musi jednak tego robi; kod skompiluje si tak samo poprawnie, jeeli wartoci
zwracane zostan cakowicie pominite. Co wicej, moe to prowadzi do pominicia
krytycznych bdw, ktre wprawdzie nie daj natychmiast katastrofalnych rezultatw,
ale potrafi przyczai si w zakamarkach aplikacji, by ujawni si w najmniej
spodziewanym momencie.
Mechanizm wyjtkw jest skonstruowany zupenie przeciwnie. Tutaj nie trzeba si
wysila, aby bd da zna o sobie, bowiem wyjtek zawsze wywoa jak reakcj -
choby nawet awaryjne zakoczenie programu. Natomiast wiadome zignorowanie
wyjtku wymaga z kolei pewnego wysiku.
Tak wic tutaj mamy do czynienia z sytuacj, w ktrej to nie programista szuka bedu,
lecz bd szuka programisty. Jest to naturalnie znacznie lepsza sytuacja z punktu
widzenia niezawodnoci programu, bo pozwala na atwiejsze odszukanie wystpujcych
we bdw.
Wyjtki 471
Naduywanie wyjtkw
Czytajc o zaletach wyjtkw, nie mona wpa w bezkrytyczny zachwyt nad nimi. One
nie s ani obowizkow technik programistyczn, ani te nie s lekarstwem na bdy w
programach, ani nawet nie s pasujcym absolutnie wszdzie rozwizaniem. Wyjtkw
atwo mona naduy i dlatego chc si przed tym przestrzec.
Nie uywajmy ich tam, gdzie wystarcz inne konstrukcje
Pocztkujcy programici maj czasem skonno do uwaania, i kade
niepowodzenie wykonania jakiego zadania zasuguje na rzucenie wyjtku. Oto (zy)
przykad:
// funkcja wyszukuje liczb w tablicy
unsigned Szukaj(const CIntArray& aTablica, int nLiczba)
{
// ptla porwnuje kolejne elementy tablicy z szukan liczb
for (unsigned i = 0; i < aTablica.Rozmiar{]; ++i)
if (aTablica[i] == nLiczba)
return i;
// w razie niepowodzenia - wyjtek?...
throw CError(__FILE__, __LINE__, "Nie znaleziono liczby");
}
Rzucanie wyjtku w razie nieznalezienia elementu tablicy to gruba przesada. Pomylmy
tylko, e kod wykorzystujcy t funkcj musiaby wyglda mniej wicej tak:
// szukamy liczby nZmienna w tablicy aTablicaLiczb
try
{
unsigned uIndeks = Szukaj(aTablicaLiczb, nZmienna);
// zrb co ze znalezion liczb...
}
catch (CError& Wyjatek)
{
std::cout << Wyjatek.Komunikat() << std::endl;
}
Moe i ma on swj urok, ale chyba lepiej skorzysta z mniej urokliwej, ale na pewno
prostszej instrukcji if, porwnujcej po prostu rezultat funkcji Szukaj() z jak ustalon
sta (np. -1), oznaczajc niepowodzenie szukania. Pozwoli to na wydorbnienie
sytuacji faktycznie wyjtkowych od tych, ktre zdarzaj si w normalnym toku dziaania
programu. Nieobecno liczby w tablicy naley zwykle do tej drugiej grupy i nie jest
wcale krytyczna dla funkcjonowania aplikacji - ergo: nie wymaga zastosowania
wyjtkw.
Nie uywajmy wyjtkw na si
Nareszcie, musz powstrzyma wszystkich tych, ktrzy z zapaem rzucili si do
implementacji wyjtkw w swych gotowych i dziaajcych programach. Niesusznie!
Prawdopodobnie bdzie to kawa cikiej, nikomu niepotrzebnej roboty. Nie ma sensu jej
wykonywa, poniewa zysk zwykle bdzie nieadekwatny do woonego wysiku.
Co najwyej mona pokusi si o zastosowanie wyjtkw w przypadku, gdy nowa wersja
danego programu wymaga napisania jego kodu od nowa. Decyzja o tym, czy tak ma si
sta w istocie, powinna by podjta jak najwczeniej.
Zaawansowane C++ 472
***
Praktyczne wykorzystanie wyjtkw to sztuka, jak zreszt cae programowanie.
Najlepszym nauczycielem bdzie tu dowiadczenie, ale jeli zawarto tego podrozdziau
pomoe ci cho troch, to jego cel bd mg uwaa za osignity.
Podsumowanie
Ten rozdzia omawia mechanizm wyjtkw w jzyku C++. Rozpocz si od
przedstawienia kilku popularnych sposobw radzenia sobie z bdami, jakie moga
wystapi w trakcie dziaania programu. Pniej poznae same wyjtki oraz podstawowe
informacje o nich. Dalej zajlimy si zagadnieniem odwijania stosu i jego konsekwencji,
by wreszcie nauczy si wykorzystywa wyjtki w praktyce.
Pytania i zadania
Rozdzia koczymy tradycyjn porcj pyta i wicze.
Pytania
1. Kiedy moemy mwi, i mamy do czynienia z sytuacj wyjtkow?
2. Dlaczego specjalny rezultat funkcji nie zawsze jest dobr metod informowania o
bdzie?
3. Czy rni si throw od return?
4. Dlaczego kolejno blokw catch jest wana?
5. Jaka jest rola bloku catch(...)?
6. Czym jest specyfikacja wyjtkw? Co dzieje si, jeeli zostanie ona naruszona?
7. Ktre obiekty s niszczone podczas odwijania stosu?
8. W jakich funkcjach nie naley rzuca wyjtkw?
9. W jaki sposb moemy zapewni zwolnienie zasobw w przypadku wystpienia
wyjtku?
10. Dlaczego warto definiowa wasne klasy dla obiektw wyjtkw?
wiczenia
1. Zastanw si, jakie informacje powinien zawiera dobry obiekt wyjtku. Ktre z
tych danych dostarcza nam sam kompilator, a ktre trzeba zapewni sobie
samemu?
2. (Trudne) Mechanizm wyjtkw zosta pomylany do obsugi bdw w trakcie
dziaania programu. To jednak nie s jego jedyne moliwe zastosowanie; pomyl,
do czego potencjalnie przydatne mog by jeszcze wyjtki - a szczeglnie
towarzyszcy im proces odwijania stosu
4
SZABLONY
Gdy co si nie udaje, mwimy,
e to by tylko eksperyment.
Robert Penn Warren
Nieuchronnie, wielkimi krokami, zbliamy si do koca kursu C++. Przed tob jeszcze
tylko jedno, ostatnie i arcywane zagadnienie: tytuowe szablony.
Ten element jzyka, jak chyba aden inny, wzbudza wrd wielu programistw rne
niezdrowe emocje i kontrowersje; porwna je mona tylko z reakcjami na preprocesor.
Nie s to aczkolwiek reakcje skrajnie negatywne: przeciwnie, szablony powszechnie
uwaa si za jeden z najwikszych atutw jzyka C++.
Problemem jest jednak to, i obecne ich moliwoci (mimo e ju teraz ogromne) s
niezadowalajce dla biegych programistw. Dlatego te wanie szablony s t czci
C++, ktra najszybciej podlega ewolucji. Trzeba jednak uwiadomi sobie, e od
odgrnie narzuconego pomysu Komitetu Standaryzacyjnego do implementacji stosownej
funkcji w kompilatorach wiedzie bardzo daleka droga. Skutek jest taki, e na palcach
jednej rki mona policzy kompilatory, ktre w peni odpowiadaj tym zaleceniom i
oferuje szablony cakowicie zgodne ze standardem. Jest to zadziwiajce, zwaywszy e
sama idea szablonw liczy ju sobie kilkanacie (!) lat.
Mam jednak take pocieszajc wiadomo. Ot mona krci nosem i narzeka, e
kompilator, ktrego uywamy, nie jest w peni na czasie, lecz dla wikszoci
programistw nie bdzie to miao wielkiego znaczenia. Oczywicie, najlepiej jest uywa
zawsze najnowszych wersji narzdzi programistycznych; nie oznacza to wszake, e
starsze ich wersje nie nadaj si do niczego.
Skoro ju o tym mwi, to przydaoby si wspomnie, jak wyglda obsuga szablonw w
naszym ulubionym kompilatorze, czyli Visual C++. I tu czeka nas raczej mia
niespodzianka. Przede wszystkim warto wiedzie, e jego aktualna wersja, zawarta w
pakiecie Microsoft Visual Studio .NET 2003, jest absolutnie zgodna z aktualnym
standardem jzyka C++ - naturalnie, take pod wzgldem obsugi szablonw. Jeeli
natomiast chodzi o starsz wersj Visual Studio .NET (nazywan teraz czsto .NET 2001),
to tutaj sprawa take przedstawia si nie najgorzej. W codziennym, ani nawet nieco
bardziej egzotycznym programowaniu nie odczujemy bowiem adnego niedostatku w
obsudze szablonw przez ten kompilator.
Niestety, podobnie dobrych wiadomoci nie mam dla uytkownikw Visual C++ 6. To
leciwe ju rodowisko moe szybko okaza si niewystarczajce. Warto wic zaopatrzy
w jego nowsz wersj.
W kadym jednak przypadku, niezalenie od posiadanego kompilatora, znajomo
szablonw jest niezbdna. Wpisay si one w praktyk programistyczn na tyle silnie, e
obecnie mao ktry program moe si bez nich obej. Poza tym przekonasz si wkrtce
na wasnej skrze, e stosowanie szablonw zdecydowanie uatwia typowe czynnoci
koderskie i sprawia, e tworzony kod staje si znacznie bardziej uniwersalny i elastyczny.
Najlepszym przykadem tego jest Biblioteka Standardowa jzyka C++, z ktrej
fragmentw miae ju okazj korzysta.
Zabierzmy si zatem do poznawania szablonw - na pewno tego nie poaujesz :D
Zaawansowane C++ 474
Podstawy
Na pocztek przedstawi ci, czym w ogle s szablony i pokae kilka przykadw na ich
zastosowanie. Bardziej zaawansowanymi zagadnieniami zajmiemy si bowiem w
nastpnym podrozdziale. Na razie czas na krtkie wprowadzenie.
Idea szablonw
Mgbym teraz podwin rkami, poprosi ci o uwag i kawaek po kawaku wyjania,
czym s te cae szablony. Na to rwnie przyjdzie pora, ale najpierw lepiej chyba odkry,
do czego mog nam si te dziwne twory przyda. Dziki temu moe atwiej przyjdzie ci
ich zrozumienie, a potem znajdowanie dla zastosowa i wreszcie polubienie ich! Tak,
szablony naprawd mona polubi - za robot, ktrej nam oszczedzaj; nam: ciko
przecie pracujcym programistom ;-)
Zobacz zatem, jakie fundamentalne problemy pomog ci niedugo rozwizywa te
nieocenione konstrukcje
ciso C++ powodem blu gowy
Pewnie syszae ju wczeniej, e C++ jest jzykiem o cisej kontroli typw. Znaczy to,
e typy danych peni w nim due znaczenie i e zawsze istnieje wyrane rozgraniczenie
pomidzy nimi.
Jednoczenie wiele mechanizmw tego jzyka suy, paradoksalnie, wanie zatarciu
granic pomidzy typami danych. Wystarczy przypomnie chociaby niejawne konwersje,
ktre pozwalaj dokonywa w locie zamiany z jednego typu na drugi, w sposb
niezauwaalny. Ponadto klasy w C++ s skonstruowane tak, aby w razie potrzeby mogy
niemal doskonale imitowa typy wbudowane.
Mimo to, ciy podzia informacji na liczby, napisy, struktury itd. moe by czsto spor
przeszkod
Dwa typowe problemy
Kopoty zaczynaj si, gdy chcemy napisa kod, ktry powinien dziaa w odniesieniu do
kilku moliwych typw danych. Z grubsza mona tu rozdzieli dwie sytuacje: gdy
prbujmy napisa uniwersaln funkcj i gdy podobn prb czynimy przy definiowaniu
klasy.
Problem 1: te same funkcje dla rnych typw
Tradycyjnym, wrcz klasycznym przykadem tego pierwszego problemu jest funkcja
wyznaczajca wiksza liczb spord dwch podanych. Prawdopodobnie z takiej funkcji
bdziesz czsto skorzysta, wic kiedy moesz j zdefiniowa np. jako:
int max(int nLiczba1, int nLiczba2)
{
return (nLiczba1 > nLiczba2 ? nLiczba1 : nLiczba2);
}
Taka funkcja dziaa dobrze dla liczb cakowitych, ale ju cakiem nie radzi sobie z liczbami
typu float czy double, bo zarwno wynik, jak i parametry s zaokrglane do jednoci.
Dla zdefiniowanych przez nas typw danych jest za zupenie nieprzydatna, co chyba
zreszt cakowicie zrozumiae.
Naturalnie, moemy sobie doda inne, przecione wersje funkcji - jak chociaby tak:
double max(double fLiczba1, double fLiczba2)
{
Szablony 475
return (fLiczba1 > fLiczba2 ? fLiczba1 : fLiczba2);
}
Takich wersji musiaoby by jednak bardzo wiele: za kadym kolejnym typem, dla
ktrego chcielibymy stosowa max(), musiaaby i odrbna funkcja. Ich definiowanie
byoby uciliwe i nudne, a podczas wykonywania tej nucej czynnoci trudno byoby nie
zwtpi, czy jest to aby na pewno suszne rozwizanie
Problem 2: klasy operujce na dowolnych typach danych
Innym problemem s klasy, ktre z jakich wzgldw musz by elastyczne i operowa
na danych dowolnego typu. Koronnym przykadem s pojemniki, jak np. tablice
dynamiczne, podobne do naszej klasy CIntArray. Jak wiemy, ma ona spor wad: przy
jej pomocy nie mona bowiem zarzdza tablic elementw innego typu ni int. Chcc
to osign, naleaoby napisa now klas - zapewne bardzo podobn do wspomnianej.
T sam prac trzebaby wykona dla kadego nastpnego typu elementw
To na pewno nie jest dobre wyjcie!
Moliwe rozwizania
Ale jakie mamy wyjcie?, spytasz pewnie. C, mona sobie jako radzi
Wykorzystanie preprocesora
Ogln funkcj max() (i podobne) moemy zasymulowa przy uyciu parametryzowanych
makr:
#define MAX(a,b) ((a) > (b) ? (a) : (b))
Sdz jednak, e pamitasz wady takich makrodefinicji. Nawiasy wok a i b likwiduj
wprawdzie problem pierwszestwa operatorw, ale nie zabezpiecz przed podwjnym
obliczaniem wyrae. Wiesz przecie, e preprocesor dziaa na kodzie tak jak na tekcie,
zatem np. wyraenie w rodzaju:
MAX(10, rand())
nie zwrci nam wcale liczby pseudolosowej rwnej co najmniej 10. Zostanie ono bowiem
rozwinite do:
((10) > (rand()) ? 10 : (rand()))
Funkcja rand() bdzie wic obliczana dwukrotnie, z kadym razem dajc oczywicie inny
wynik - bo takie jest jej przeznaczenie. Makro MAX() nie bdzie wic zawsze dziaao
poprawnie.
Uywanie oglnych typw
Jeszcze mniej oczywisty jest sposb na zaimplementowanie oglnej klasy, np. tablicy
przechowujcej dowolny typ elementw. Tutaj aczkolwiek take istnieje pewne
rozwizanie: mona uy oglnego wskanika, tworzc tablic elementw typu void*:
class CPtrArray
{
private:
// tablica i jej rozmiar
void** m_ppvTablica;
unsigned m_uRozmiar;
// itd. (metody i przecione operatory)
Zaawansowane C++ 476
};
Bdziemy musieli si jednak zmaga z niedogodnociami wskanikw void* - przede
wszystkim z utrat informacji o rzeczywistym typie danych:
CPtrArray Tablica(5);
// alokacja pamici dla elementu (!)
Tablica[2] = new int;
// przypisanie - nieszczeglnie adne...
*(static_cast<int*>(Tablica[2])) = 10;
Kadorazowe rzutowanie na waciwy typ elementw (tutaj int) na pewno nie bdzie
naleao do przyjenoci. Poza tym trzeba bdzie pamita o zwolnieniu pamici
zaalokowanej dla poszczeglnych elementw. W przypadku maych obiektw, jak liczby,
nie ma to adnego sensu
Zatem nie! To na pewno nie jest zadowalajce wyjcie!
Szablony jako rozwizanie
W porzdku, dosy tych bezowocnych poszukiwa. Myl, e domylasz si, i to
szablony s tym rozwizaniem, ktrego poszukujemy. Zatem nie tracc wicej czasu,
znajdmy je wreszcie :)
Kod niezaleny od typu
Wrmy wpierw do prb napisania funkcji max(). Patrzc na jej dwie wersje, dla typw
int i double, moemy atwo zauway, e rni si one bardzo niewiele. Waciwie to
mona stwierdzi, e po prostu drugi z wariantw ma wpisane double tam, gdzie w
pierwszym widnieje typ int.
Gdybymy wic chcieli napisa oglny wzorzec dla funkcji max(), wygldaby on tak:
typ max(typ Parametr1, typ Parametr2)
{
return (Parametr > Parametr2 ? Parametr1 : Parametr2);
}
No dobrze, moemy sobie pisa takie wzorce, ale co nam z tego? Nie znamy przecie
adnego sposobu, aby przekaza go kompilatorowi do wykorzystania Czy na pewno?
Kompilator to potrafi
Ale nie! Moemy ten wzorzec - ten szablon (ang. template) - wpisa do kodu, tworzc
ogln funkcj max(). Trzeba to jedynie zrobi w odpowiedni sposb - tak, aby
kompilator wiedzia, z czym ma do czynienia. Zobaczmy wic, jak mona tego dokona.
Skadnia szablonu
A zatem: chcc zdefiniowa wzorzec funkcji max(), musimy napisa go w ten oto sposb
sposb:
template <typename TYP> TYP max(TYP Parametr1, TYP Parametr2)
{
return (Parametr1 > Parametr2 ? Parametr1 : Parametr2);
}
Szablony 477
Dopki nie wyjanimy sobie dokadnie kwestii umieszczania szablonw w plikach
rdowych, zapamitaj, aby wpisywa je w caoci w plikach nagwkowych.
W ten sposb tworzymy szablon funkcji (ang. function template) Zobaczmy, co si na
niego skada.
Zauwaye zapewne najpierw zupenie now cz nagwka funkcji:
template <typename TYP>
Jest ona obowizkowa dla kadego rodzaju szablonw, nie tylko funkcji. Sowo kluczowe
template (szablon) mwi bowiem kompilatorowi, e nie ma tu do czynienia ze zwykym
kodem, lecz wanie z szablonem.
Dalej nastpuje, ujta w nawiasy ostre, lista parametrw szablonu. W tym przypadku
mamy tylko jeden taki parametr: sowo typename (nazwa typu) informuje, e jest nim
typ. Okazuje si bowiem, e parametrami szablonu mog by take normalne wartoci,
podobne do argumentw funkcji - nimi te si zajmiemy, ale pniej. Na razie mamy tu
jeden parametr szablonu bdcy typem o jake opisowej nazwie TYP.
Potem przychodzi ju normalna definicja funkcji - z jedn drobn rnic. Jak wida,
uywamy w niej nazwy TYP zamiast waciwego typu danych (czyli int, double, itd.).
Stosujemy go jednak w tych samych miejscach, czyli jako typ wartoci zwracanej oraz
typ obu przyjmowanych parametrw funkcji.
Tre szablonu odpowiada wic wzorcowi z poprzedniego akapitu. Rnica jest jednak
taka, e o ile tamten kod by niezrozumiay dla kompilatora, o tyle ten szablon jest jak
najbardziej poprawny i, co najwaniejsze, dziaa zgodnie z oczekiwaniami. Nasza funkcja
max() potrafi ju bowiem operowa na dowolnym typie argumentw:
int nMax = max(-1, 2); // TYP = int
unsigned uMax = max(10u, 65u); // TYP = unsigned
float fMax = max(-12.4, 67); // TYP = double (!)
Najciekawsze jest to, i to funkcja na podstawie swych argumentw sama zgaduje, jaki
typ danych ma by wstawiony w miejsce symbolicznej nazwy TYP. To wanie jedna z
zalet szablonw funkcji: uywamy ich zwykle tak samo, jak normalnych funkcji, a
jednoczenie zyskujemy zadziwiajc uniwersalno.
Popatrzmy jeszcze na ogln skadni szablonu w C++:
template <parametry_szablonu> kod
Jak wspomniaem, swko template jest tu obowizkowe, bo dziki nim niemu kompilator
wie, e ma do czynienia z szablonem. parametry_szablonu to najczciej symboliczne
oznaczenia nieznanych z gry typw danych; oznaczenia te s wykorzystywane w
nastpujcym dalej kodzie.
Na temat obu tych kluczowych czci szablonu powiemy sobie jeszcze mnstwo rzeczy.
Co moe by szablonem
Wpierw ustalmy, do jakiego rodzaju kodu w C++ moemy doczepi fraz
template<...>, czynic j szablonem. Generalnie mamy dwa rodzaje szablonw:
szablony funkcji - s to wic taki funkcje, ktre mog dziaa w odniesieniu do
dowolnego typu danych. Zazwyczaj kompilator potrafi bezbdnie ustali, jaki typ
jest waciwy w konkretnym wywoaniu (por. przykad zastosowania szablonu
max() z poprzedniego punktu)
Zaawansowane C++ 478
szablony klas - czyli klasy, potrafice operowa na danych dowolnego typu. W tym
przypadku musimy zwykle poda ten waciwy typ; zobaczymy to wszystko nieco
dalej
Wkrtce aczkolwiek okazao si, e bardzo podane s take inne rodzaje szablonw -
gwnie po to, aby uatwi prac z szablonami klas. My jednak zajmiemy si zwaszcza
tymi dwoma rodzajami szablonw. Wpierw wic poznasz nieco bliej szablony funkcji, a
potem zobaczysz take szablony klas.
Szablony funkcji
Szablon funkcji moemy wyobrazi sobie jako:
oglny algorytm, ktry dziaa poprawnie dla danych rnego typu
zesp funkcji, zawierajc odrbne wersje funkcji dla poszczeglnych typw
Oba te podejcia s cakiem suszne, aczkolwiek jedno z nich bardziej odpowiada
rzeczywistoci. Ot:
Szablon funkcji reprezentuje zestaw (rodzin) funkcji, dziaajcych dla dowolnej liczby
typw danych.
Zasada stojca za szablonami jest taka, e kompilator sam dokonuje po prostu tego, co
mgby zrobi programista, nudzc si przy tym niezmiernie. Na podstawie szablonu
funkcji generowane s wic jej konkretne egzemplarze (specjalizacje, bdce
przecionymi funkcjami), operujce ju na rzeczywistych typach danych. Potem s one
wywoywane w trakcie dziaania programu.
Proces ten nazywamy konkretyzacj (ang. instantiation) i zachodzi on dla wszelkiego
rodzaju szablonw. Zanim aczkolwiek moe do niego doj, szablno trzeba zdefiniowa.
Zobaczmy wic, jak definiuje si szablony funkcji.
Definiowanie szablonu funkcji
Definicja szablonu funkcji nie rni si zbytnio od zwykej definicji funkcji. Ot, po prostu
jeden typ (lub wicej) nie s w niej podane explicit, lecz wnioskowane z wywoania
funkcji szablonowej. Niemniej, temu wszystkiemu trzeba si przyjrze bliej.
Podstawowa definicja szablonu funkcji
Oto jeden z prostszych chyba przykadw szablonu funkcji - warto bezwzgldna:
template <typename TYP> TYP Abs(TYP Liczba)
{
return (Liczba >= 0 ? Liczba : -Liczba);
}
Posiada takiego szablonu ma t niezaprzeczaln zalet, e bez dodatkowego wysiku
moemy posugiwa si t funkcj dla liczb dowolnego typu: int, float, double, itd. Co
najwaniejsze, w wyniku otrzymamy warto tego samego typu, co podany parametr,
zatem nie musimy posugiwa si rzutowaniem - co byoby konieczne w przypadku
zdefiniowania zwykej funkcji dla najbardziej pojemnego typu double.
Dlaczego tak jest? Oczywicie dlatego, i symboliczne oznaczenie TYP (czyli parametr
szablonu) wystpuje zarwno jako typ wartoci zwracanej, jak i typ parametru funkcji.
W konkretnych egzemplarzach funkcji w obu miejscach wystpi wic ten sam typ, np.
int.
Szablony 479
Stosowalno definicji
Mona zapyta: Czy powyszy szablon moe dziaa tylko dla wbudowanych typw
liczbowych? Czy poradziby sobie np. z wyznaczeniem wartoci bezwzgldnej z liczby
wymiernej, czyli obiektu zdefiniowanej ongi klasy CRational?
Aby zdecydowa o tym i o podobnych sprawach, musimy odpowiedzie na inne pytanie:
Czy to, co robimy w treci szablonu funkcji, da si wykona po podstawieniu danego typu w miejsce
parametru szablonu?
U nas wic typ danych, wystpujcy na razie pod oznaczeniem TYP, musi udostpnia:
operator porwnania >=, pozwalajcy na konfrontacj obiektu z zerem
operator negacji -, sucy tutaj do uzyskania liczby przeciwnej do danej
publiczny konstruktor kopiujcy, umoliwiajcy zwrot wyniku funkcji
Pod wszystkie te wymagania podpadaj rzecz jasna wbudowane typy liczbowe. Jeli za
wyposaylibymy klas CRational we dwa wspomniane operatory, to take jej obiekty
mogyby by argumentami funkcji Abs()! Wynika std, e:
Szablon funkcji moe by stosowany dla tych typw danych, dla ktrych poprawne
s wszystkie operacje, dokonywane na obiektach tyche typw w treci szablonu.
atwo mona wic stwierdzi, e np. dla typu std::string ten szablon byby
niedozwolony. Klasa std::string nie udostpnia bowiem operatora negacji, ani te nie
pozwala na porwnywanie swych obiektw z liczbami cakowitymi.
Parametr szablonu uyty w ciele funkcji
Trudno zauway to na pierwszy rzut oka, ale przedstawiony wyej szablon ma jeden
do powany zgrzyt. Mianowicie, wymusza on na podanym mu typie danych, aby
pozwala na porwnywanie go z typem int. Do takiego typu naley bowiem newralgiczne
0.
Nie jest to zbyt dobre i lepiej, eby funkcja nie korzystaa z takiego rozwizania.
Interpretacja zera w rnych typach liczbowych moe by bowiem cakiem odmienna od
zakadanej przez nas.
Lepiej wic, eby punkt zerowy mg by ustalony przez domylny konstruktor.
Wwczas szablon bdzie wyglda tak - zmiana jest niewielka:
template <typename TYP> TYP Abs(TYP Liczba)
{
return (Liczba >= TYP() ? Liczba : -Liczba);
}
Teraz bdzie on jednak dziaa poprawnie dla kadego sensownego typu danych
liczbowych.
Chwileczk, rzekniesz. A co z typami podstawowymi? Przecie one nie maj
konstruktorw! Faktycznie, suszna uwaga. Tak uwag poczyni pewnie swego czasu
ktry z twrcw C++, gdy zaowacowaa ona wprowadzeniem do jzyka tzw.
inicjalizacji zerowej. Jest to bardzo prosta rzecz: ot typy wbudowane (jak int czy
bool) zostay wyposaone w swego rodzaju konstruktory. Nie s to prawdziwe funkcje
skadowe, jak w przypadku klas, lecz po prostu moliwo uycia tej samej skadni
jawnego wywoania domylnego konstruktora. Wyglda ona tak:
typ()
Zaawansowane C++ 480
i dla klas nie jest, jak sdz, adn niespodziank. To samo jednak moemy uczyni
take w stosunku do podstawowych typw danych. W C++ s wic cakowicie poprawne
wyraenia typu int(), float(), bool() czy unsigned(). Co waniejsze w wyniku daj
one zero odpowiedniego typu - czyli dziaaj tak, jakbymy napisali (odpowiednio): 0,
0.0f, false i 0u.
Inicjalizacja zerowa gwarantuje wic wspprac naszego szablonu z typami
podstawowymi, poniewa wyraenie TYP() da w kadym przypadku potrzebny nam tutaj
obiekt zerowy. Niewane, czy bdzie chodzio o typ podstawowy C++, czy te klas
zdefiniowan przez programist.
Parametr szablonu i parametr funkcji
Mwic o szablonach funkcji, mona si nieco zagubi w znaczeniu sowa parametr.
Mamy mianowicie a dwa rodzaje parametrw:
parametry funkcji - czyli te znane nam ju od dawna, bo wystpuje one w kadej
niemal funkcji. Kady taki parametr ma swj typ i nazw
parametry szablonu poznalimy w tym rozdziale. W przypadku szablonw funkcji
mog to by wyacznie nazwy typw. Parametry szablonu stosujemy wic w
nagwku i w ciele funkcji tak, jak gdyby byy to nazwy typw, np. float czy
VECTOR2D
To naturalne, e oba te rodzaje parametrw s ze sob cile zwizane. Popatrzmy
choby na nagwek funkcji max():
template <typename TYP> TYP max(TYP Parametr1, TYP Parametr2)
Parametry tej funkcji to Parametr1 i Parametr2. Obydwa nale one do typu
oznaczonego po prostu jako TYP. w TYP mgby by klas, aliasem zdefiniowanym
poprzez typedef, wyliczeniem enum, itd. Tutaj jednak TYP jest parametrem szablonu:
deklarujemy go w nawiasach ostrych po sowie template przy pomocy typename.
Fakt, e TYP parametrw funkcji jest parametrem szablonu ma dalekosine i
dobroczynne konsekwencje. Powoduje to mianowicie, i moe on by wydedukowany z
argumentw wywoania funkcji:
// (byo ju do przykadw wywoywania max(), wic jeden wystarczy :D)
std::cout << max(42, 69);
Nie musimy w powyszej linijce wyranie okrela, e szablon max() ma by tu uyty do
wygenerowania funkcji pracujcej na argumentach typu int. Ten typ zostanie po prostu
wzity z argumentw wywoania (ktre s typu int wanie). To jedna z wielkich zalet
szablonw funkcji.
Moliwe jest aczkolwiek jawne okrelenie typu, czyli parametru szablonu. O tym powiemy
sobie w nastpnym paragrafie.
Kilka parametrw szablonu
Dotd widzielimy jednoparametrowe szablony funkcji, ale nie jest to kres moliwoci
szablonw. Tak naprawd bowiem mog mie one dowoln liczb parametrw. Oto na
przykad inny wariant funkcji max():
template <typename TYP1, typename TYP2>
TYP1 max(TYP1 Parametr1, TYP2 Parametr2)
{
return (Parametr > Parametr2 ? Parametr1 : Parametr2);
}
Szablony 481
Podobnie jak parametry funkcji, parametry szablonu zawarte w nawiasach ostrych take
o oddzielamy przecinkami. Moe ich by dowolna ilo; tutaj mamy dwa parametry
szablonu, ktre bezporednio przedkadaj si na dwa parametry funkcji. Nowa wersja
funkcji max() potrafi wic porwnywa wartoci rnych typw - o ile oczywicie istnieje
odpowiedni operator >.
Oto przykad wykorzystania tego szablonu:
int nMax = max(-18, 42u); // TYP1 = int, TYP2 = unsigned
float fMax = max(9.5f, 34); // TYP1 = float, TYP2 = int
fMax = max(6.78, 80); // TYP1 = double, TYP2 = int
W ostatnim wywoaniu wartoci zwrcon przez max() bdzie 80.0 typu double. Jej
przypisanie do mniej pojemnego typu float spowoduje zapewne ostrzeenie
kompilatora.
Jak wida, argumenty funkcji nie musz by tu konwertowane do wsplnego typu, jak to
si dziao przy jednoparametrowym szablonie. W sumie jednak midzy oboma
szablonami nie ma wielkiej rznicy funkcjonalnej; podaem tu jedynie przykad na to, e
szablon funkcji moe mie wicej parametrw ni jeden.
Z powyszym szablonem jest jednak pewien do istotny kopot. Chodzi mianowicie o typ
wartoci zwracanej. Wpisaem w nim wprawdzie TYP1, ale to nie ma adnego
uzasadnienia, gdy rwnie dobry (a raczej niedobry) byy TYP2.
Problemem jest to, i na etapie kompilacji nie wiemy rzecz jasna, jakie wartoci zostan
przekazane do funkcji. Nie wiemy wobec tego, jaki powinien by typ wartoci zwracanej.
W takiej sytuacji naleaoby uy typu oglniejszego, bardziej pojemnego: dla int i
float byby to zatem float, i tak dalej (przypomnij sobie z poprzedniego rozdziau,
kiedy jaki typ jest oglniejszy od drugiego). Niestety, poniewa z samego zaoenia
szablonw funkcji nie wiemy, dla jakich faktycznych typw bdzie on uyty, nie moemy
nijak okreli, ktry z tej dwjki bdzie pojemniejszy. W zasadzie wic nie wiemy, jaki
powinien by typ wartoci zwracanej!
Rozsdne rozwizanie tego problemu nie ley niestety w zakresie moliwoci
programisty. Potrzebny jest tutaj jaki nowy mechanizm jezyka; zwykle mwi si w tym
kontekcie o operatorze typeof (typ czego). Miaby on zwraca nazw typu z podanego
mu (staego) wyraenia. Nazwa ta mogaby by potem uyta tak, jak kada inna nazwa
typu - a wic na przykad w charakterze rodzaju wartoci zwracanej przez funkcj.
Obecnie istniej kompilatory, ktre oferuj operator typeof, ale oficjalny standard C++
pki co nic o nim nie mwi.
Specjalizacja szablonu funkcji
Podstawowy szablon funkcji definiuje nam ogln rodzin funkcji, ktrej czonkowie
(specjalizacje) dla kadego typu (parametru szablonu) zachowuj si tak samo. Nasza
funkcja max() bdzie wic zwracay wiksz liczb niezalenie od tego, czy typem jest
liczby bdzie double czy int.
Powiesz: I bardzo dobrze! O to nam przecie chodzi. No tak, ale jest pewien szkopu.
Dla pewnych typw danych algorytm wyznaczania wikszej wartoci moe by
nieodpowiedni. Uoglniajc spraw, mona zkonkludowa, e niekiedy potrzebna nam
jest specjalna wersja szablonu funkcji, ktra dla jakiego konkretnego typu (parametru
szablonu) bdzie si zachowywaa inaczej ni dla reszty.
Wtedy wanie musimy sami zdefiniowa ow konkretn specjalizacj szablonu
funkcji. Tym zajmiemy si w niniejszym paragrafie.
Zaawansowane C++ 482
Wyjtkowy przypadek
Twoja nauka C++ opiera si midzy innymi na serii narzuconych przypuszcze, zatem
teraz przypumy, e chcemy rozszerzy nieco funkcjonalno szablonu funkcji max().
Zalmy mianowicie, e chcemy uczyni j wadn do wsppracy nie tylko z liczbami, ale
te z tak oto klas wektora:
#include <cmath>
struct VECTOR2
{
// wsprzdne tego wektora
double x, y;
//-------------------------------------------------------------------
// metoda liczca dugo wektora
double Dlugosc() const { return sqrt(x * x + y * y); }
// (reszta jest rednio potrzebna, zatem pomijamy)
};
Naturalnie, monaby wyposay j w odpowiedni operator>(). My jednak chcemy
zdefiniowa specjalizowan wersj szablonu funkcji max(). Czynimy to w taki oto sposb:
template<> VECTOR2 max(VECTOR2 vWektor1, VECTOR2 vWektor2)
{
// porwujemy dugoci wektorw; w przypadku rwnoci zwracamy 1-szy
return (vWektor1.Dlugosc() >= vWektor2.Dlugosc() ?
vWektor1 : vWektor2);
}
Waciwie to mona powiedzie, e funkcja ta nie rni si prawie niczym od normalnej
funkcji max() (nieszablonowej). Dlatego te wane jest opatrzenie jej fraz template<>
(z pustymi nawiasami ostrymi), bo dziki temu kompilator moe uzna nasza definicj za
specjalizacj szablonu funkcji max().
Co do nagwka funkcji, to jest to ten sam naglwek, co w oryginalnym szablonie - z t
tylko rnic, e TYP zostao zamienione na nazw rzeczywistego typu, czyli VECTOR2. Ze
wzgldu na t jednoznaczno specjalizacja nie wymaga adnych dalszych zabiegw. W
sumie jednak mona (i zaleca si) bezporednie podanie typu, dla ktrego specjalizujemy
szablon:
template<> VECTOR2 max<VECTOR2>(VECTOR2 vWektor1, VECTOR2 vWektor2)
Dziwn fraz max<VECTOR2> mona tu z powodzeniem traktowa jako nazw funkcji -
specjalizacji szablonu max() dla typu VECTOR2. W takiej zreszt roli poznamy podobne
konstrukcje, gdy zajmiemy si dokadniej uyciem funkcji szablonowych.
Ciekawostka: specjalizacja czciowa szablonu funkcji
Jak kada Ciekawostka, take i ta nie jest przeznaczona dla pocztkujcych, a ju na
pewno nie podczas pierwszego kontaktu z tekstem.
Poprzednio specjalizowalimy funkcj dla cile okrelonego typu danych. Teoretycznie
monaby jednak zrobi co innego: napisa specjaln jej wersj dla pewnego rodzaju
typw.
No, teraz to ju przesadzasz!, moesz tak odpowiedzie. To jednak moe mie sens;
wyobramy sobie, e przy pomocy max() sprbujemy porwna dwa wskaniki. Co
Szablony 483
otrzymamy w wyniku takiego porwnania? Naturalnie, dostaniemy ten wskanik,
ktrego adres jest mniejszy.
Zapytam wprost: i co nam z tego? Lepiej chyba byoby, aby porwnanie dokonywane
byo raczej na obiektach, do ktrych te wskaniki si odnosz. Wtedy mielibymy
bardziej sensowny wynik i np. z dwch wskanikw typu int* dostalibymy ten, ktry
odnosi si do wikszej liczby.
Takie dziaanie szablonu funkcji max() w odniesieniu do wskanikw - przy zachowaniu
jego normalnego dziaania dla pozostaych typw danych - nie jest moliwe do
osignicia przy pomocy zwykej specjalizacji, zaprezentowanej w poprzednim punkcie.
Trzebaby bowiem zdefiniowa osobne wersje dla wszystkich typw wskanikw (int*,
CRational*, float*, ), jakich chcielibymy uywa. Cakowicie przekrela to sens
szablonw, ktre przecie opieraj si wanie na tym, e to sam kompilator generuje ich
wyspecjalizowane wersje w zalenoci od potrzeb.
Tutaj trzeba by uy mechanizmu specjalizacji czciowej, znanego bardziej z
szablonw klas. Oznacza on ni mniej, ni wicej, jak tylko zdefiniowanie innej wersji
szablonu dla caej grupy typw (parametrw szablonu). W tym przypadku ta grup s
typy wskanikowe, a szablon funkcji max() wygldaby dla nich tak:
template <typename TYP>
TYP* max<TYP*>(TYP* pWskaznik1, TYP* pWskaznik2)
{
return (*pWskaznik1 > *pWskaznik2 ? pWskaznik1 : pWskaznik2);
}
Nazwa specjalizowanej funkcji, czyli max<TYP*>, gdzie TYP jest parametrem szablonu,
wskazuje jednoznacznie, i chodzi nam o wersj funkcji przeznaczon dla wskanikw.
Naturalnie, typ wartoci zwracanej i parametrw funkcji musi by rwnie taki sam.
Kiedy zostanie uyty ten bardziej wyspecjalizowany szablon? Ot wtedy, gdy jako
parametry funkcji max() zostan przekazane jakie wskaniki, np.:
int nLiczba1 = 10, nLiczba2 = 98;
int* pnLiczba1 = &nLiczba1;
int* pnLiczba2 = &nLiczba2;
std::cout << *(max(pnLiczba1, pnLiczba2)); // szablon max<TYP*>(),
// gdzie TYP = int
W tym wic przypadku wywietlan liczb bdzie zawsze 98, bo liczy si bd tutaj
faktyczne wartoci, a nie rozmieszczenie zmiennych w pamici (a wic nie adresy, na
ktre pokazuj wskaniki).
Czciowe specjalizacje szablonw funkcji nie wygldaj moe na zbytnio
skomplikowane. Moe ci jednak zaskoczy to, i to jeden z najbardziej zaawansowanych
aspektw szablonw - tak bardzo, e pki co Standard C++ o nim nie wspomina (!), a
tylko nieliczne kompilatory obsuguj go. Pki co jest to wic bardzo rzadko uywana
technika i dlatego na razie naley j traktowa jako ciekawostk.
Wywoywanie funkcji szablonowej
Skoro ju mniej wicej wiemy, jak mona definiowa szablony funkcji, nauczmy si teraz
z nich korzysta. Zwaywszy, e ju to robilimy, nie powinno to sprawia adnych
trudnoci.
Zastanwmy si jednak, co dzieje si w momencie wywoania funkcji szablonowej. Oto
przykad takiego wywoania:
Zaawansowane C++ 484
max(12, 56)
max() jest tu szablonem funkcji, ktrego parametr (typ) jest stosowany w charakterze
typu obu parametrw funkcji, jak rwnie zwracanej przez ni warto. Nie podajemy
jednak tego typu dosownie; to wanie wielka zaleta szablonw funkcji, gdy waciwy
typ - parametr szablonu, tutaj int - moe by wydedukowany z jej wywoania. O tym,
jak to si dzieje, mwi nastpny akapit.
Aby jednak zrozumie istot szablonw funkcji, musimy cho z grubsza wiedzie, jak
kompilator traktuje takie wywoania jak powysze. Generalnie nie jest trudne. Jak
wspomniaem wczeniej, szablony w C++ s implementowane w ten sposb, i podczas
kompilacji tworzony jest ich waciwy (nieszablonowy) kod dla kadego typu, dla
ktrego uywamy danego szablonu. Proces ten nazywamy konkretyzacj
(ang. instantiation) a poszczeglne egzemplarze szablonw - specjalizacjami
(ang. specialization albo instance).
Tak wic kompilator musi sobie wytworzy odpowiednie specjalizacje, ktre bd
wykorzystywane w miejscach uycia szablonu. W przykadzie powyej szablon funkcji
max() posuy do wygenerowania jej konkretnej wersji: funkcji max() dla parametru
szablonu rwnego int. Dopiero ta konkretna wersja - specjalizacja - bdzie
skompilowana w normalny sposb, do normalnego kodu maszynowego. W ten sposb
zarwno funkcje, jak te klasy szablonowe zachowuj niemal wszystkie cechy zwykych
funkcji i klas.
To, jak szablon funkcji zostanie skonkretyzowany w danym przypadku, zaley wycznie
od sposobu jego uycia w kodzie. Przyjrzyjmy si wic sposobom na wywoywanie funkcji
szablonowych.
Jawne okrelenie typu
Zwykle uywajc szablonw funkcji pozwalamy kompilatorowi na samodzielne
wydedukowanie typu, dla ktrego ma on by skonkretyzowany. Zdarza si jednak, e
chcemy go sami wyranie okreli. To rwnie jest moliwe.
Wywoywanie konkretnej wersji funkcji szablonowej
Moemy wic zayczy sobie, aby funkcja max() dziaaa w danym przypadku,
powiedzmy, na liczbach typu unsigned - mimo e typem jej argumentw bdzie int:
unsigned uMax = max<unsigned>(45, 3); // 45 i 3 to liczby typu int
Skadnia max<unsigned> pozwala nam poda dany typ. cilej mwic, w nawiasach
ostrych podajemy parametry szablonu (w odrnieniu od parametrw funkcji,
podanych jak zwykle w nawiasach okrgych). Tutaj jest to jeden parametr, bdcy
typem; nadajemy mu warto unsigned, czyli typu liczb bez znaku.
Takie wywoanie powoduje, e nie jest ju przeprowadza adna dedukacja typu
argumentw funkcji. Kompilator nie zwaa ju na nie, lecz oczekuje, e bd one
zgadzay si z typem pdoanym jawnie - parametrem szablonu. W tym wic przypadku
liczby musz pasowa do typu unsigned i oczywicie pasuj do niego (s dodatnie), cho
ich waciwy typ to int. Nie gra on jednak adnej roli, gdy sami odgrnie narzucilimy
tutaj parametr szablonu.
Uycie wskanika na funkcj szablonow
max<unsigned> wystpuje tutaj w miejscu, gdzie zwykle pojawia si nazwa funkcji w
przypadku normalnych procedur. To nie przypadek: moemy t fraz traktowa wanie
jako nazw funkcji - konkretnej ju funkcji, a nie jej szablonu.
Szablony 485
Nie jest to adne pustosowie, bowiem ma to konkretne konsekwencje. Nazwa
max<unsigned> dziaa mianowicie tak samo, jak kada inna nazwa funkcji. W
szczeglnoci, moemy jej uy do pobrania adresu funkcji szablonowej:
unsigned (*pfnUIntMax)(unsigned, unsigned) = max<unsigned>;
Zauwa rnic: nie moemy pobra adresu szablonu (czyli max), bo ten nie istnieje w
pamici podczas dziaania programu. Jest on tylko instrukcj dla kompilatora (podobnie
jak makra s instrukcjami dla preprocesora), mwic mu, jak ma wygenerowa
prawdziwe, specjalizowane funkcje. max<unsigned> jest tak wanie wyspecjalizowan
funkcj i ona ju istnieje w pamici, bowiem jest kompilowana do kodu maszynowego
tak, jak normalna funkcja. Moemy zatem pobra jej adres.
Dedukcja typu na podstawie argumentw funkcji
Jawne podawanie parametrw szablonu funkcji jest generalnie nieczsto stosowane.
Zdecydowanie najwiksz zalet tych szablonw jest to, i potrafi same wykry typ
argumentw funkcji i na tej podstawie dopasowa odpowiedni parametr szablonu.
Spjrzmy, jak to si odbywa.
Jak to dziaa
A zatem, skd kompilator wie, dla jakich parametrw ma skonkretyzowa szablon
funkcji? Innymi sowy, skd bierze on waciwy typ dla funkcji szablonowej? C, nie
jest to bardzo skomplikowane:
Parametry szablonu funkcji s dedukowane w oparciu o parametry jej wywoania
oraz niejawne konwersje.
Przeledmy to na przykadzie wywoania szablonu funkcji:
template <typename TYP> TYP max(TYP Parametr1, TYP Parametr2);
w kilku formach:
max(67, 76) // 1
max(5.6, 6.5f) // 2
max(8.7f, 9.0f) // 3
max("Hello", std::string("world")) // 4
Pierwszy przykad jest jak sdze prosty. Obie liczby s tu typu int, zatem uyt tu
funkcj max<int>. Nie ma adnych watpliwoci.
Dalej jest ciekawiej. Parametry drugiego wywoania funkcji s typu double i float.
Mamy jednak jeden parametr szablonu (TYP), ktry musi przyj t sam warto w
wywoaniu funkcji. Co zatem zrobi kompilator? Wykorzysta on to, e midzy float i
double istnieje niejawna konwersja i wybierze typ double jako oglniejszy. Uytym
wariantem bdzie wic max<double>.
Kolejny przykad to nic nowego :) Oba argumenty s tu typu float (skutek przyrostka
f), zatem wykorzystan funkcj bdzie max<float>.
Ostatnia, czwarta linijka jest zdecydowanie najciekawsza. Napisy "Hello" i "world"
maj z pewnoci ten sam typ - const char[]. Niemniej, drugi parametr jest typu
std::string, bowiem jawnie tworzymy obiekt tej klasy przy uyciu konstruktora. Wobec
takiego obrotu sprawy kompilator musi pogodzi go z const char[]. Robi to, poniewa
Zaawansowane C++ 486
istnieje niejawna konwersja ancucha typu C na std::string. Szablon funkcji zostanie
wic skonkretyzowany do max<std::string>
120
.
Oglny wniosek z tych przykadw jest taki, e jeli jeden parametr szablonu musi by
dopasowany na podstawie kilku rnych typw parametrw funkcji, to kompilator
prbuje zastosowa niejawne konwersje celem sprowadzenia ich do jakiego jednego
typu oglnego. Dopiero jeeli ta prba si nie powiedzie, sygnalizowany jest bd.
W zasadzie to trzeba powiedzie: jeeli ta prba si nie powiedzie i nie ma adnych
innych moliwych dopasowa. Moliwe bowiem, e istniej inne szablony, ktrych
parametry pozwalaj na problematyczne dopasowanie. Przykadowo, wywoanie max(18,
"tekst") nie mogoby by dopasowane do jednoparametrowego szablonu max(), ale bez
problemu przypasowane zostaoby do szablonu dwuparametrowego max(), podanego
jaki czas temu (i poniej). Ten dopuszczaby przecie rne typy argumentw.
Regua mwica, i pierwsze niepowodzenie dopasowywania parametrw szablonu nie
jest bdem, funkcjonuje pod skrtem SFINAE (ang. Substitution Failure Is Not An Error -
poraka podstawiania nie jest bdem).
Dedukcja przy wykorzystaniu kilku parametrw szablonu
Proces dedukcji zaczyna nabiera rumiecw, gdy mamy do czynienia z szablonem o
wikszej liczbie parametrw ni jeden. Przypomnijmy sobie szablon funkcji max() z
dwoma parametrami (deklaracj tylko, bo definicja jest chyba oczywista):
template <typename TYP1, typename TYP2>
TYP1 max(TYP1 Parametr1, TYP2 Parametr2);
Tutaj wszystko jest nawet znacznie prostsze ni poprzednio. Dziki temu, e kady
parametr funkcji ma swj wasny typ (parametr szablonu), kompilator ma uatwione
zadanie. Nie musi ju bra pod uwag adnych niejawnych konwersji.
Z powyszym szablonem zwizanym jest jednak pewien problem. Nie bardzo wiadomo,
jaki ma by typ zwracany tej funkcji. Moe to by zarwno TYP1, jak i TYP2 - zaley po
prostu, ktra z wartoci zwyciy w tecie porwnawczym. Tego jego nie sposb ustali
w czasie kompilacji; mona jednak doda typ oddawany do parametrw szablonu:
template <typename TYP1, typename TYP2, typename ZWROT>
ZWROT max(TYP1 Parametr1, TYP2 Parametr2);
Prba wywoania tej funkcji w zwykej formie zakoczy si jednak bdem - a to dlatego,
e ten nowy, trzeci parametr nie moe zosta wydedukowany przez kompilator! Mwiem
przecie, e dedukcja dokonywana jest wycznie na podstawie parametrw funkcji.
Warto zwracana si zatem nie liczy.
Hmm, to nie jest a taki problem, odpowiesz moe. Ten jeden parametr mog przecie
poda; wpisze tam po prostu typ oglniejszy spord dwch poprzedzajcych. Tak si
jednak nie da! Nie moemy poda do szablonu ostatniego parametru, gdy wpierw
musielibymy poda dwa poprzedzajce go:
max<int, float, float>(17, 67f);
To chyba adna niespodzianka: analogicznie jest z parametrami funkcji. W ten sposb
tracimy jednak wszystkie wspaniaoci automatycznej dedukcji parametrw szablonu.
120
Porwnywanie dwch napisw moe si wydawa dziwne, ale jest ono poprawne. Klasa std::string
posiada operator >, dokonujcy porwnania tekstw pod wzgldem ich dugoci oraz przechowywanych we
znakw (ich kolejnoci alfabetycznej).
Szablony 487
Istnieje aczkolwiek sposb na to. Naley przesun parametr ZWROT na pocztek listy
parametrw szablonu:
template <typename ZWROT, typename TYP1, typename TYP2>
ZWROT max(TYP1 Parametr1, TYP2 Parametr2);
Teraz pozostae dwa typy mog by odgadnite z parametrw funkcji. Tego szablonu
max() bdziemy wic mogli uywa, podajc tylko typ wartoci zwracanej:
max<float>(17, 67f);
Wynika std prosty wniosek:
Dedukcja parametrw szablonu nastpuje od koca (od prawej strony). Te parametry,
ktre mog by wzite z wywoania funkcji, powinny zatem znajdowa si na kocu listy.
Szablony klas
Szablony funkcji mog przedstawia si wcale zachcajco, jednak o wiele wiksz zalet
C++ s szablony klas. Ponownie, moemy je traktowa jako:
swego rodzaju oglne klasy (zwane czasem metaklasami), definiujce zachowanie
si obiektw w odniesieniu do dowolnych typw danych
zesp klas, delegujcych odrbne klasy do obsugi rnych typw
Po raz kolejny te to drugie podejcie jest bardziej poprawne.
Szablon klasy reprezentuje zestaw (rodzin) klas, mogcych wsppracowa z rnymi
typami danych.
Konieczno istnienia szablonw klas bezporednio wynika z faktu, e C++ jest jzykiem
zorientowanym obiektowo. Do potrzeb programowania strukturalnego z pewnoci
wystarczyyby szablony funkcji; kiedy jednak chcemy w peni korzysta z dobrodziejstw
OOPu i cieszy si elastycznoci szablonw, naturalnym jest uycie szablonw klas.
Z bardziej praktycznego punktu widzenia szablony klas s znacznie przydatniejsze i
czciej stosowane ni szablony funkcji. Typowym ich zastosowaniem s klasy
pojemnikowe, czyli znane i lubiane struktury danych - a one obok algorytmw, s wedug
klasykw informatyki podstawowymi skadnikami programw. Niemniej przez lata
istnienia szablony klas dorobiy si take wielu cakiem niespodziewanych zastosowa.
Szablony klas intensywnie wykorzystuje Biblioteka Standardowa jzyka C++, a take
niezwykle popularna biblioteka Boost.
Niezalenie od tego, czy twj kontakt z tymi rodzajami szablonw bdzie si ogranicza
wycznie do pojemnikw w rodzaju wektorw lub kolejek, czy te wymylisz dla nich
znacznie wicej zastosowa, powiniene dobrze pozna ten element jzyka C++. I te
temu wanie suy niniejsza sekcja.
Definicja szablonu klas
Wpierw wic zajmiemy si definiowaniem szablonu klasy. Popatrzmy sobie najpierw na
prosty przykad szablonu, bdcy rozszerzeniem klasy CIntArray, przewijajcej si przez
kilka poprzednich rozdziaw. Dalej zajmiemy si te bardziej zaawansowanymi
aspektami definicji szablonw klas.
Zaawansowane C++ 488
Prosty przykad tablicy
W rozdziale o wskanikach pokazaem ci prost klas dynamicznej tablicy int-w -
CIntArray. Wtedy interesowaa nas dynamiczna alokacja pamici, wic nie przeszkadza
nam fakt nieporcznoci teje klasy. Miaa ona bowiem dwa mankamenty: nie pozwalaa
na uycie nawiasw kwadratowych [] celem dostpu do elementw tablicy, no i potrafia
przechowywa wycznie liczby typu int.
Obiecaem jednoczenie, e w swoim czasie pozbdziemy si obu tych niedogodnoci.
Miae si ju okazj przekona, e nie rzucam sw na wiatr, bowiem nauczylimy ju
nasz klas poprawnie reagowa na operator []. Zapewne domylasz si, e teraz
usuniemy drugi z mankamentw i wyposaymy j w moliwo przechowywania
elementw dowolnego typu. Jak nietrudno zgadn, bdzie to wymagao uczynienia jej
szablonem klasy.
Zanim przystpimy do dziea, spjrzmy na aktualn wersj naszej klasy:
class CIntArray
{
// domylny rozmiar tablicy
static const unsigned DOMYSLNY_ROZMIAR = 5;
private:
// wskanik na waciw tablic oraz jej rozmiar
int* m_pnTablica;
unsigned m_uRozmiar;
public:
// konstruktory
explicit CIntArray(unsigned uRozmiar = DOMYSLNY_ROZMIAR)
: m_uRozmiar(uRozmiar);
m_pnTablica(new int [m_uRozmiar]) { }
CIntArray(const CIntArray&);
// destruktor
~CIntArray() { delete[] m_pnTablica; }
//-------------------------------------------------------------
// pobieranie i ustawianie elementw tablicy
int Pobierz(unsigned uIndeks) const
{ if (uIndeks < m_uRozmiar) return m_pnTablica[uIndeks];
else return 0; }
bool Ustaw(unsigned uIndeks, int nWartosc)
{ if (uIndeks >= m_uRozmiar) return false;
m_pnTablica[uIndeks] = uWartosc;
return true; }
// inne
unsigned Rozmiar() const { return m_uRozmiar; }
//-------------------------------------------------------------
// operator indeksowania
int& operator[](unsigned uIndeks)
{ return m_pnTablica[uIndeks]; }
// operator przypisania (duszy, wic nie w definicji)
CIntArray& operator=(const CIntArray&);
};
Szablony 489
Przerbmy j zatem na szablon.
Definiujemy szablon
Jak wic zdefiniowa szablon klasy w C++? Patrzc na ogln skadni szablonu mona
by nawet domyli si tego, lecz spjrzmy na poniszy - pusty na razie - przykad:
template <typename TYP> class TArray
{
// ...
};
Jest to szkielet definicji szablonu klasy TArray, czyli tablicy dynamicznej na elementy
dowolnego typu
121
. Wida tu znane ju czci: przede wszystkim, fraza template
<typename TYP> identyfikuje konstrukcj jako szablon i deklaruje parametry tego
szablonu. Tutaj mamy jeden parametr - bdzie nim rzecz jasna typ elementw tablicy.
Dalej mamy waciwie zwyk definicj klasy i w zasadzie jedyn dobrze widoczn rnic
jest to, e wewntrz niej moemy uy nazwy TYP - parametru szablonu. U nas bdzie
on peni identyczn rol jak int w CIntArray, zatem pena wersja szablonu TArray
bdzie wygldaa nastpujco:
template <typename TYP> class TArray
{
// domylny rozmiar tablicy
static const unsigned DOMYSLNY_ROZMIAR = 5;
private:
// wskanik na waciw tablic oraz jej rozmiar
TYP* m_pTablica;
unsigned m_uRozmiar;
public:
// konstruktory
explicit TArray(unsigned uRozmiar = DOMYSLNY_ROZMIAR)
: m_uRozmiar(uRozmiar),
m_pTablica(new TYP [m_uRozmiar]) { }
TArray(const TArray&);
// destruktor
~TArray() { delete[] m_pTablica; }
//-------------------------------------------------------------
// pobieranie i ustawianie elementw tablicy
TYP Pobierz(unsigned uIndeks) const
{ if (uIndeks < m_uRozmiar) return m_pTablica[uIndeks];
else return TYP(); }
bool Ustaw(unsigned uIndeks, TYP Wartosc)
{ if (uIndeks >= m_uRozmiar) return false;
m_pTablica[uIndeks] = Wartosc;
return true; }
// inne
unsigned Rozmiar() const { return m_uRozmiar; }
//-------------------------------------------------------------
121
Litera T w nazwie TArray to skrt od template, czyli szablon.
Zaawansowane C++ 490
// operator indeksowania
TYP& operator[](unsigned uIndeks)
{ return m_pTablica[uIndeks]; }
// operator przypisania (duszy, wic nie w definicji)
TArray& operator=(const TArray&);
};
Moesz by nawet zaskoczony, e byo to takie proste. Faktycznie, uczynienie klasy
CIntArray szablonem ograniczao si do zastpienia nazwy int, uytej jako typ
elementw tablicy, nazw parametru szablonu - TYP. Pamitaj jednak, e nigdy nie
powinno si bezmylnie dokonywa takiego zastpowania; int mg by przecie choby
typem licznika ptli for (for (int i = ...)) i w takiej sytuacji zastpienie go przez
parametr szablonu nie miaoby adnego sensu. Nie zapominaj wic, e jak zwykle
podczas programowania naley myle nad tym, co robimy.
Naturalnie, gdy ju opanujesz szablony klas (co, jak sdz, stanie si niedugo),
dojdziesz do wniosku, e wygodniej jest od razu definiowia waciwy szablon ni
wychodzi od specjalizowanej klasy i czyni j ogln.
Implementacja metod poza definicj
Szablon jest ju prawie gotowy. Musimy jeszcze doda do niego implementacje dwch
metod: konstruktora kopiujcego i operatora przypisania - ze wzgldu na ich dugo
lepiej bdzie, jeli znajd si poza definicj. W przypadku zwykych klas byo to jak
najbardziej moliwe a jak jest dla szablonw?
Zapewne nie jest niespodziank to, i rwnie tutaj jest to dopuszczalne. Warto jednak
uwiadomi sobie, e metody szablonw klas s szablonami metod. Oznacza to ni
mniej, ni wicej, ale to, i powinnimy je traktowa podobnie, jak szablony funkcji. Wie
si z tym gwnie inna skadnia.
Popatrz wic na przykad - oto szablonowa wersja konstruktora kopiujcego:
template <typename TYP> TArray<TYP>::TArray(const TArray& aTablica)
{
// alokujemy pami
m_uRozmiar = aTablica.m_uRozmiar;
m_pTablica = new TYP [m_uRozmiar];
// kopiujemy pami ze starej tablicy do nowej
memcpy (m_pTablica, aTablica.m_pTablica, m_uRozmiar * sizeof(TYP));
}
I znowu moemy mie dja vu: kod zaczynamy ponownie sekwencj template <...>.
atwo to jednak uzasadni: mamy tu bowiem do czynienia z szablonem, w ktrym
uywamy przecie jego parametru TYP. Koniecznie wic musimy uyc wspomnianej
sekwencji po to, aby:
kompilator wiedzia, e ma do czynienia z szablonem, a nie zwykym kodem
moliwe byo uycie nazw parametrw szablonu (tutaj mamy jeden - TYP) w jego
wntrzu
Kady kawaek szablonu trzeba zatem zacz od owego template <...>, aby te dwa
warunki byy spenione. Jest to moe i uciliwe, lecz niestety konieczne.
Idmy dalej - zostajc jednak nadal w pierwszym wierszu kodu. Jest on nader
interesujcy z tego wzgldu, e a trzykrotnie wystpuje w nim nazwa naszego szablonu,
TArray - na dodatek ma ona tutaj trzy rne znaczenia. Przenalizujmy je:
Szablony 491
w pierwszym przypadku jest to wyraz TArray<TYP>. Jak pamitamy z szablonw
funkcji, takie konstrukcje oznaczaj zazwyczaj konkretne egzemplarze szablonu -
specjalizacje. W tym jednak wypadku podajemy tu parametr TYP, a nie jaki
szczeglny typ danych. W sumie cay ten zwrot peni funkcj nazwy typu klasy;
potraktuj to po prostu jako obowizkow cz nagwka, wystpujc zawsze
przed operatorem :: w implementacji metod. Podobnie byo np. z CIntArray, gdy
chodzio o zwyke metody zwykych klas. Zapamitaj zatem, e:
Sekwencja nazwa_szablonu<typ> peni rol nazwy typu klasy tam, gdzie jest to
konieczne.
drugi raz uywamy TArray w charakterze nazwy metody - konstruktora. Moe to
nie by nieco mylce, bo przecie piszc konstruktory normalnych klas po obu
stronach operatora zasigu podawalimy t sam nazw. Musisz wic zapamita,
e:
Konstruktory i destruktory w szablonach klas maj nazwy odpowiadajce nazwom
ich macierzystych szablonw i niczemu wicej, tzn. nie zawieraj parametrw w
nawiasach ostrych.
trzeci raz TArray jest uyta jako cz typu parametru konstruktora kopiujcego -
const TArray&. By moe zabyniesz tu kompetencj i krzykniesz, e to
niepoprawne i e jeli chodzi nam o nazw typu klasy szablonowej, to powinnimy
wstawi TArray<TYP>, bo samo TArray to tylko nazwa szablonu. Odpowiem
jednak, e posunicie to jest rwnie poprawne; mamy tu do czynienia z tak
zwan nazw wtrcon. Polega to na tym, i:
Sama nazwa szablonu moe by stosowana wewntrz niego w tych miejscach, gdzie
wymagany jest typ klasy szablonowej. Moemy wic posuy si ni do skrtowego
deklarowania pl, zmiennych czy parametrw funkcji bez potrzeby pisania nawiasw
ostrych i nazw parametrw szablonu.
Wobec nagwka tak cikiego kalibru reszta tej funkcji nie przedstawia si chyba bardzo
skomplikowanie? :) W rzeczywistoci to niemal dokadna kopia treci oryginalnego
konstruktora kopiujcego - z tym, e typ int elementw CIntArray zastpuje tutaj
nieznany z gry TYP - parametr szablonu.
W podobny sposb naleaoby jeszcze zaimplementowa operator przypisania. Sdz, e
nie sprawioby ci problemu samodzielne wykonanie tego zadania.
Korzystanie z tablicy
Gdy mamy ju definiowany szablon klasy, chcielibymy zapewne skorzysta z niego.
Sprbujmy wic stworzy sobie obiekt tablicy; poniewa przez cay zajmowalimy si
tablic int-w, to teraz niech bdzie to tablica napisw:
TArray<std::string> aNapisy(3);
Jak doskonale wiemy, to co widnieje po lewej stronie jest typem deklarowanej zmiennej.
W tym przypadku jest to wic TArray<std::string> - specjalizowana wersja naszego
szablonu klas. Uywamy w niej skadni, do ktrej, jak sdz, zaczynasz si ju
przyzwyczaja. Po nazwie szablonu (TArray) wpisujemy wic par nawiasw ostrych, a w
niej warto parametru szablonu (typ std::string). U nas parametr ten okrela
jednoczenie typ elementw tablicy - powysza linijka tworzy wic trjelementow
tablic acuchw znakw.
Cakiem podobnie wyglda tworzenie tablicy ze zmiennych innych typw, np.:
Zaawansowane C++ 492
TArray<float> aLiczbyRzeczywiste(7); // 7-el. tablica z liczbami float
TArray<bool> aFlagi(8) // zestaw omiu flag bool-owskich
TArray<CFoo*> aFoo; // tablica wskanikw na obiekty
Zwrmy uwag, e parametr(y) szablonu - tutaj: typ elementw tablicy - musimy
poda zawsze. Nie ma moliwoci wydedukowania go, bo i skd? Nie jest to przecie
funkcja, ktrej przekazujemy parametry, lecz obiekt klasy, ktry tworzymy.
Postpowanie z tak tablic nie rni si niczym od posugiwania si klas CIntArray, a
wic porednio - rwnie zwykymi tablicami w C++. W szablonach C++ obowizuj po
prostu te same mechanizmy, co w zwykych klasach: dziaaj przecione operatory,
niejawne konwersje i reszta tych nietuzinkowych moliwoci OOPu. Korzystanie z
szablonw klas jest wic nie tylko efektywne i elastycznie, ale i intuicyjne:
// wypenienie tablicy
aNapisy[0] = "raz";
aNapisy[1] = "dwa";
aNapisy[2] = "trzy";
// pokazanie zawartoci tablicy
for (unsigned i = 0; i < aNapisy.Rozmiar(); ++i)
std::cout << aNapisy[i] << std::endl;
Przyznasz chyba teraz, e szablony klas przedstawiaj si wyjtkowo zachcajco?
Dowiedzmy si zatem wicej o tych konstrukcjach.
Dziedziczenie i szablony klas
Nowy wspaniay wynalazek jzyka C++ - szablony - moe wsppracowa ze starym
wspaniaym wynalazkiem jzyka C++ - dziedziczeniem. A tam, gdzie spotykaj si dwa
wspaniae wynalazki, musi by doprawdy cudownie :) Zajmijmy si wic dziedziczeniem
poczonym z szablonami klas.
Dziedziczenie klas szablonowych
Szablony klas (jak TArray) s podstawami do generowania specjalizowanych klas
szablonowych (jak np. TArray<int>). Ten specjalizowane klasy zasadniczo niczym nie
rni si od innych uprzednio zdefiniowanych klas. Mog wic na przykad by klasami
bazowymi dla nowych typw.
Czas na ilustracj zagadnienia w postaci przykadowego kodu. Oto klasa wektora liczb:
class CVector : public TArray<double>
{
public:
// operator mnoenia skalarnego
double operator*(const CVector&);
};
Dziedziczy ona z TArray<double>, czyli zwykej tablicy liczb. Dodaje ona jednak
dodatkow metod - przeciony operator mnoenia *, obliczajcy iloczyn skalarny:
double CVector::operator*(const CVector& aWektor)
{
// jeeli rozmiary wektorw nie s rwne, rzucamy wyjtek
if (Rozmiar() != aWektor.Rozmiar())
throw CError(__FILE__, __LINE__, "Blad iloczynu skalarnego");
// liczymy iloczyn
Szablony 493
double fWynik = 0.0;
for (unsigned i = 0; i < Rozmiar(); ++i)
fWynik += (*this)[i] * aWektor[i];
// zwracamy wynik
return fWynik;
}
W samym akcie dziedziczenia, jak i w implementacji klasy pochodnej, nie ma adnych
niespodzianek. Uywamy po prostu TArray<double> tak, jak kadej innej nazwy klasy i
moemy korzysta z jej publicznych i chronionych skadnikw. Naley oczywicie
pamita, e w tej klasie typ double wystpuje tam, gdzie w szablonie TArray pojawia
si parametr szablonu - TYP. Dotyczy to chociaby rezultatu operatora [], ktry jest
wanie liczb typu double:
fWynik += (*this)[i] * aWektor[i];
Myl aczkolwiek, e fakt ten jest intuicyjny i dziedziczenie specjalizowanych klas
szablonowych nie bdzie ci sprawia kopotu.
Dziedziczenie szablonw klas
Szablony i dziedziczenie umoliwiaj rwnie tworzenie nowych szablonw klas na
podstawie ju istniejcych, innych szablonw. Na czym polega rnica? Ot na tym, e
w ten sposb tworzymy nowy szablon klas, a nie pojedyncz, zwyk klas - jak to si
dziao poprzednio. Wtedy definiowalimy normaln klas przy pomocy innej, niemale
normalnej klasy - rnica bya tylko w tym, e t klas bazow bya specjalizacja
szablonu (TArray<double>). Teraz natomiast bdziemy konstruowali szablon klas
pochodnych przy uyciu szablonu klas bazowych. Cay czas bdziemy wic porusza
si w obrebie czysto szablonowego kodu z nasz ulubion fraz template <...> ;)
Oto nasz nowy szablon - tablica, ktra potrafi dynamicznie zmienia swj rozmiar w
czasie swego istnienia:
template <typename TYP> class TDynamicArray : public TArray<TYP>
{
public:
// funkcja dokonujca ponownego wymiarowania tablicy
bool ZmienRozmiar(unsigned);
};
Poniewa jest to szablon, wic rozpoczynamy go od zwyczajowego pocztku i listy
parametrw. Nadal bdzie to jeden TYP elementw tablicy, ale nic nie staoby na
przeszkodzie, aby lista parametrw szablonu zostaa w jaki sposb zmodyfikowana.
W dalszej kolejnoci widzimy znajomy pocztek definicji klasy. Jako klas bazow
wstawiamy tu TArray<TYP>. Przypomina to poprzedni punkt, ale pamitajmy, e teraz
korzystamy z parametru szablonu (TYP) zamiast z konkretnego typu (double). Nazwa
klasy bazowej jest wic tak samo zszablonowana jak caa reszta definicji
TDynamicArray.
Pozostaje jeszcze kwestia implementacji metody ZmienRozmiar(). Nie powinna by ona
niespodzienka, bowiem wiesz ju, jak kodowa metody szablonw klas poza blokiem ich
definicji. Tre funkcji jest natomiast niemal wiern kopi tej z rozdziau o wskanikach:
template <typename TYP>
bool TDynamicArray<TYP>::ZmienRozmiar(unsigned uNowyRozmiar)
{
// sprawdzamy, czy nowy rozmiar jest wikszy od starego
Zaawansowane C++ 494
if (!(uNowyRozmiar > m_uRozmiar)) return false;
// alokujemy now tablic
TYP* pNowaTablica = new TYP [uNowyRozmiar];
// kopiujemy do star tablic i zwalniamy j
memcpy (pnNowaTablica, m_pTablica, m_uRozmiar * sizeof(TYP));
delete[] m_pTablica;
// "podczepiamy" now tablic do klasy i zapamitujemy jej rozmiar
m_pTablica = pNowaTablica;
m_uRozmiar = uNowyRozmiar;
// zwracamy pozytywny rezultat
return true;
}
Widzimy wic, e dziedziczenie szablonu klasy nie jest wcale trudne. W jego wyniku
powstaje po prostu nowy szablon klas.
Deklaracje w szablonach klas
Pola i metody to najwaniejsze skadniki definicji klas - take tych szablonowych. Jeeli
jednak chodzi o szablony, to znacznie czciej moemy tam spotka rwnie inne
deklaracje. Trzeba si im przyjrze, co teraz uczynimy.
Ten paragraf moesz pomin przy pierwszym podejciu do lektury, jeli wyda ci si zbyt
trudny, i przej dalej.
Aliasy typedef
Cech wyrniajc szablony jest to, i operuj one na typach danych w podobny
sposb, jak inny kod na samych danych. Naturalnie, wszystkie te operacje s
przeprowadzane w czasie kompilacji programu, a ich wiksz czci jest konkretyzacja -
tworzenie specjalizowanych wersji funkcji i klas na podstawie ich szablonw.
Proces ten sprawia jednoczenie, e niektre przewidywalne i, zdawaoby si, znajome
konstrukcje jzykowe nabieraj nowych cech. Naley do nich choby instrukcja typedef;
w oryginale suy ona wycznie do tworzenia alternatywnych nazw dla typw np. tak:
typedef void* PTR;
Nie jest to adna rewolucja w programowaniu, co zreszt podkrelaem, prezentujc t
instrukcj. Ciekawie zaczyna si robi dopiero wtedy, jeli uwiadomimy sobie, e
aliasowanym typem moe by parametr szablonu! Ale skd on pochodzi?
Oczywicie - z szablonu klasy. Jeeli bowiem umiecimy typedef wewntrz definicji
takiego szablonu, to moemy w niej wykorzysta parametryzowany typ. Oto najprostszy
przykad:
template <typename TYP> class TArray
{
public:
// alias na parametr szablonu
typedef TYP ELEMENT;
// (reszta niewana)
};
Szablony 495
Instrukcja typedef pozwala nam wprowadzenie czego w rodzaju skadowej klasy
reprezentujcej typ. Naturalnie, jest to tylko skadowa w sensie przenonym, niemniej
nazwa ELEMENT zachowuje si wewntrz klasy i poza ni jako penoprawny typ danych -
rwnowany parametrowi szablonu, TYP.
Przydatno takiego aliasu moe si aczkolwiek wydawa wtpliwa, bo przecie atwiej i
krcej jest pisa nazw typu float ni TArray<float>::ELEMENT. typedef wewntrz
szablonu klasy (lub nawet oglnie - w odniesieniu do szablonw) ma jednak znacznie
sensowniejsze zastosowania, gdy wsppracuje ze soba wiele takich szablonw.
Koronnym przykadem jest Biblioteka Standardowa C++, gdzie w ten sposb cakiem
mona zyska dostp m.in. do tzw. iteratorw, wspomagajcym prac ze strukturami
danych.
Deklaracje przyjani
Czciej spotykanym elementem w zwykych klasach s deklaracje przyjani. Naturalnie,
w szablonach klas nie moglo ich zabrakn. Moemy tutaj rwnie deklarowa przyjanie
z funkcjami i klasami.
Dodatkowo moliwe jest (obsuguj to nowsze kompilatory) uczynienie deklaracji
przyjani szablonow. Oto przykad:
template <typename T> class TBar { /* ... */ };
template <typename T> class TFoo
{
// deklaracja przyjani z szablonem klasy TBar
template <typename U> friend class TBar<U>;
};
Taka deklaracja sprawia, e wszystkie specjalizacje szablonu TBar bd zaprzyjanione
ze wszystkimi specjalizacjami szablonu TFoo. TFoo<int> bdzie wic miaa dostp do
niepublicznych skadowych TBar<double>, TBar<unsigned>, TBar<std::string> i
wszystkich innych specjalizacji szablonu TBar.
Zauwamy, e nie jest to rwnowaznaczne z zastosowaniem deklaracji:
friend class TBar<T>;
Ona spowoduje tylko, e zaprzyjanione zostan te egzemplarze szablonw TBar i TFoo,
ktre konkretyzowano z tym samym parametrem T. TBar<float> bdzie wic
zaprzyjaniony z TFoo<float>, ale np. z TFoo<short> czy z jakkolwiek inn
specjalizacj TFoo ju nie.
Szablony funkcji skadowych
Istnieje bardzo ciekawa moliwo
122
: metody klas mog by szablonami. Naturalnie,
moesz pomyle, e to adna nowo, bo przecie w przypadku szablonw klas
wszystkie ich metody s swego rodzaju szablonami funkcji. Chodzi jednak o co innego,
co najlepiej zobaczymy na przykadzie.
Nasz szablon TArray dziaa cakiem znonie i umoliwia podstawow funkcjonalno w
zakresie tablic. Ma jednak pewn wad; spjrzmy na poniszy kod:
TArray<float> aFloaty1(10), aFloaty2;
TArray<int> aInty(7);
122
Dostpna aczkolwiek tylko w niektrych kompilatorach (np. w Visual C++ .NET 2003), podobnie jak
szablony deklaracji przyjani.
Zaawansowane C++ 496
// ...
aFloaty1 = aFloaty2; // OK, przypisujemy tablic tego samego typu
aFloaty2 = aInty; // BD! TArray<int> niezgodne z TArray<float>
Drugie przypisanie tablicy int-w do tablicy float-w nie jest dopuszczalne. To
niedobrze, poniewa, logicznie rzecz ujmujc, powinno to by jak najbardziej moliwe.
Kopiowanie mogoby si przecie odby poprzez przepisanie poszczeglnych liczb -
elementw tablicy aInty. Konwersja z int do float jest bowiem jak cakowicie
poprawna i nie powoduje adnych szkodliwych efektw.
Kompilator jednak tego nie wie, gdy w szablonie TArray zdefiniowalimy operator
przypisania wycznie dla tablic tego samego typu. Musielibymy wic doda kolejn
jego wersj - tym razem uniwersaln, szablonow. Dziki temu w razie potrzeby mona
by jej uy w takich wanie przypisaniach. Jak to zrobi? Spjrzmy:
template <typename T> class TArray
{
public:
// szablonowy operator przypisania
template <typename U>
TArray<T>& operator=(const TArray<U>&);
// (reszta niewana)
};
Mamy wic tutaj znowu zagniedon deklaracj szablonu. Druga fraza template <...>
jest nam potrzebna, aby uniezaleni od typu operator przypisania - uniezaleni nie
tylko w sensie oglnym (jak to ma miejsce w caym szablonie TArray), ale te w
znaczeniu moliwej innoci parametru tego szablonu (U) od parametru T macierzystego
szablonu TArray. Zatem przykadowo: jeeli zastosujemy przypisanie tablicy
TArray<int> do TArray<float>, to T przyjmie warto float, za U - int.
Wszystko jasne? To teraz czas na smakowity deser. Powyszy szablon metody trzeba
jeszcze zaimplementowa. No i jak to zrobi? C, nic prostszego. Napiszmy wic t
funkcj.
Zaczynamy oczywicie od template <...>:
template <typename T>
W ten sposb niejako otwieramy pierwszy z szablonw - czyli TArray. Ale to jeszcze nie
wszystko: mamy przecie w nim kolejny szablon - operator przypisania. Co z tym
pocz? Ale tak, potrzebujemy drugiej frazy template <...>:
template <typename T> // od szablonu klasy TArray
template <typename U> // od szablonu operatora przypisania
licznie to wyglda, no ale to nadal nie wszystko. Dalej jednak jest ju, jak sdz,
prosto. Piszemy bowiem zwyky nagwek metody, posikujc si prototypem z definicji
klasy. A zatem:
template <typename T>
template <typename U>
TArray<T>& TArray<T>::operator=(const TArray<U>& aTablica)
{
// ...
}
Szablony 497
Stosuj tu takie dziwne formatowanie kodu, aby unaoczni ci jego najwaniejsze
elementy. W normalnej praktyce moesz rzecz jasna skondensowa go bardziej, piszc
np. obie klauzule template <...> w jednym wierszu i nie wcinajc kodu metody.
Wreszcie, czas na ciao funkcji - to chyba najprostsza cz. Robimy podobnie, jak w
normalnym operatorze przypisania: najpierw niszczymy wasn tablic obiektu, tworzymy
now dla przypisywanej tablicy i kopiujemy jej tre:
template <typename T>
template <typename U>
TArray<T>& TArray<T>::operator=(const TArray<U>& aTablica)
{
// niszczymy wasn tablic
delete[] m_pTablica;
// tworzymy now, o odpowiednim rozmiarze
m_uRozmiar = aTablica.Rozmiar();
m_pTablica = new T [m_uRozmiar];
// przepisujemy zawarto tablicy przy pomocy ptli
for (unsigned i = 0; i < m_uRozmiar; ++i)
m_pTablica = aTablica[i];
// zwracamy referencj do wasnego obiektu
return *this;
}
Niespodzianek raczej brak - moe z wyjtkiem ptli uytej do kopiowania zawartoci. Nie
posugujemy si tutaj memcpy() z prostego powodu: chcemy, aby przy przepisywaniu
elementw zadziaay niejawne konwersje. Dokonuj si one oczywicie w linijce:
m_pTablica = aTablica[i];
To wanie ona sprawi, e w razie niedozwolonego przypisywania tablic (np.
TArray<std::string> do TArray<double>) kompilacja nie powiedzie si. Natomiast we
wszystkich innych przypadkach, jeli istniej niejawne konwersje midzy elementami
tablicy, wszystko bdzie w porzdku.
Do penego szczcia naleaoby jeszcze w podobny sposb zdefiniowa konstruktor
konwertujcy (albo kopiujcy - zaley jak na to patrze), bdcy rwnie szablonem
metody. To oczywicie zadanie dla ciebie :)
Korzystanie z klas szablonowych
Zdefiniowanie szablonu klasy to naturalnie dopiero poowa sukcesu. Pieczoowicie
stworzony szablon chcemy przecie wykorzysta w praktyce. Porozmawiajmy wic, jak to
zrobi.
Tworzenie obiektw
Najbardziej oczywistym sposobem korzystania z szablonu klasy jest tworzenie obiektw
bazujcych na specjalizacji tego szablonu.
Stwarzamy obiekt klasy szablonowej
W kreowaniu obiektw klas szablonowych nie ma niczego nadzwyczajnego; robilimy to
ju kilkakrotnie. Zobaczmy na najprostszy przykad - utworzenia tablicy elementw typu
long:
Zaawansowane C++ 498
TArray<long> aLongi;
long jest tu parametrem szablonu TArray. Jednoczenie cay wyraz TArray<long> jest
typem zmiennej aLongi. Analogia ze zwykych typw danych dziaa wic tak samo dla
klas szablonowych.
Docierajc do tego miejsca pewnie przypomniae ju sobie o wskaniku std::auto_ptr
z poprzedniego rozdziau. Patrzc na instrukcj jego tworzenia nietrudno wycign
wniosek: auto_ptr jest rwnie szablonem klasy. Parametrem tego szablonu jest za
typ, na ktry wskanik pokazuje.
Przy okazji tego banalnego punktu zwrc jeszcze uwage na pewien fakt skadniowy.
Przypumy wic, e zapragniemy stworzy przy uyciu naszego szablonu tablic
dwuwymiarow. Pamitajc o tym, e w C++ tablice wielowymiarowe s obsugiwane
jako tablice tablic, wyprodukujemy zapewne co w tym rodzaju:
TArray<TArray<int>> aInty2D; // no i co tu jest le?...
Koncepcyjnie wszystko jest tutaj w porzdku: TArray<int> jest po prostu parametrem
szablonu, czyli okrela tym elementw tablicy - mamy wic tablic tablic elementw typu
int. Nieoczekiwanie jednak kompilator wykazuje si tu kompletn ignoracj i zupenym
brakiem ogady: problematyczne staj si bowiem dwa zamykajce nawiasy ostre,
umieszczone obok siebie. S one interpretowane jako uwaga operator przesunicia
bitowego w prawo! Wiem, e to brzmi idiotycznie, bo przecie w tym kontekcie
operator ten jest zupenie niemoliwy do zastosowania. Musz wic przeprosi ci za
wikszo nierozgarnitych kompilatorw, ktre w tym kontekcie interpretuj sekwencj
>> jako operator bitowy
123
.
No dobrze, ale co z tym fantem zrobi? Ot rozwizanie jest nadzwyczaj proste: trzeba
oddzieli oba znaki, aby nie mogo ju dochodzi do nieporozumie na linii kompilator-
programista:
TArray<TArray<int> > aInty2D; // i teraz jest OK
Moe wyglda to nieadnie, ale pki co naley tak wanie pisa. Zapamitaj wic, e:
W miejsach, gdy w kodzie uywajcym szablonw maj wystpi obok siebie dwa
ostre nawiasy zamykajce (>>), naley wstawi midzy nimi spacj (> >), by nie
pozwoli na ich interpretacj jako operatora przesunicia.
O tym i o podobnych lapsusach jzykowych napomkn wicej w stosownym czasie.
Co si dzieje, gdy tworzymy obiekt szablonu klasy
Aby ten paragraf nie by jedynie prezentacj rzeczy oczywistych (tworzenie obiektw klas
szablowych) i denerwujcych (vide kwestia nawiasw ostrych), powiedzmy sobie jeszcze
o jednej sprawie. Co w zasadzie dzieje si, gdy w kodzie napotka kompilator na
instrukcj tworzc obiekt klasy szablonowej?
Oczywicie, oglna odpowied brzmi generuje odpowiedni kod maszynowy. Warto
jednak zagbi si nieco w szczegy, bo dziki temu spotka nas pewna mia
niespodzianka
A zatem - co si dzieje? Przede wszystkim musimy sobie uwiadomi fakt, e takie
nazwy jak TArray<int>, TDynamicArray<double> i inne nazwy szablonw klas z
123
To waciwie problem nie tylko kompilatora, ale samego Standardu C++, ktry pozwala im na takie
beztroskie zachowanie. Pozostaje mie nadziej, e to si zmieni
Szablony 499
podanymi parametrami nie reprezentuj klas istniejcych w kodzie programu. S
one tylko instrukcjami dla kompilatora, mwicymi mu, by wykona dwie czynnoci:
odnalaz wskazany szablon klas (TArray, TDynamicArray ) i sprawdzi, czy
podane mu parametry s poprawne
wykona jego konkretyzacj, czyli wygenerowa odpowiednie klasy szablonowe
Waciwe klasy s wic tworzone dopiero w czasie kompilacji - dziaa to na nieco
podobnej zasadzie, jak rozwijanie makr preprocesora, cho jest oczywicie znacznie
bardziej zaawansowane. Najwaniejsze dla nas, programistw nie s jednak szczegy
tego procesu, lecz jedna cecha kompilatora - bardzo dla nas korzystna.
A chodzi o to, e kompilator jest leniwy (ang. lazy)! Jego lenistwo polega na tym, e
wykonuje on wycznie tyle pracy, ile jest konieczne do poprawnej kompilacji - i nic
ponadto. W przypadku szablonw klas znaczy to po prostu tyle, e:
Konkretyzacji podlegaj tylko te skadowe klasy, ktre s faktycznie uywane.
Ten bardzo przyjemny dla nas fakt najlepiej zrozumie, jeeli przez chwil wczujemy si
w rol leniwego kompilatora. Przypumy, e widzi on tak deklaracj:
TArray<CFoo> aFoos;
Naturalnie, odszukuje on szablon TArray; przypumy, e stwierdza przy tym, i dla typu
CFoo nie by on jeszcze konkretyzowany. Innymi sowy, nie posiada definicji klasy
szablonowej dla tablicy elementw typu CFoo. Musi wic j stworzy. C wic robi? Ot
w pocie czoa generuje on dla siebie kod w mniej wicej takiej postaci
124
:
class TArray<CFoo>
{
static const unsigned DOMYSLNY_ROZMIAR = 5;
private:
CFoo* m_pTablica;
unsigned m_uRozmiar;
public:
explicit TArray(unsigned uRozmiar = DOMYSLNY_ROZMIAR)
: m_uRozmiar(uRozmiar), m_pTablica(new CFoo [m_uRozmiar]) { }
};
Chwila! A gdzie s wszystkie pozostae metody?! Moesz si zaniepokoi, ale poczekaj
chwil Powiedzmy, e oto dalej spotykamy instrukcj:
aFoos[0] = CFoo("Foooo!");
Co wtedy? Wracamy mianowicie do wygenerowanej przed chwil definicji, a kompilator j
modyfikuje i teraz wyglda ona tak:
class TArray<CFoo>
{
static const unsigned DOMYSLNY_ROZMIAR = 5;
private:
CFoo* m_pTablica;
unsigned m_uRozmiar;
124
Domniemane produkty pracy kompilatora zapisuj bez charakterystycznego formatowania.
Zaawansowane C++ 500
public:
explicit TArray(unsigned uRozmiar = DOMYSLNY_ROZMIAR)
: m_uRozmiar(uRozmiar), m_pTablica(new CFoo [m_uRozmiar]) { }
CFoo& operator[](unsigned uIndeks) { return m_pTablica[uIndeks]; }
};
Wreszcie kompilator stwierdza, e wyszed poza zasig zmiennej aFoos. Co wtedy dzieje
si z nasz klas? Spjrzmy na ni:
class TArray<CFoo>
{
static const unsigned DOMYSLNY_ROZMIAR = 5;
private:
CFoo* m_pTablica;
unsigned m_uRozmiar;
public:
explicit TArray(unsigned uRozmiar = DOMYSLNY_ROZMIAR)
: m_uRozmiar(uRozmiar), m_pTablica(new CFoo [m_uRozmiar]) { }
~TArray() { delete m_pTablica; }
CFoo& operator[](unsigned uIndeks) { return m_pTablica[uIndeks]; }
};
Czy ju rozumiesz? Przypuszczam, e tak. Zaakcentujmy jednak to wane stwierdzenie:
Kompilator konkretyzuje wycznie te metody klasy szablonowej, ktre s
uywane.
Korzy z tego faktu jest chyba oczywista: generowanie tylko potrzebnego kodu sprawia,
e w ostatecznym rozrachunku jest go mniej. Programy s wic mniejsze, a przez to
take szybciej dziaaj. I to wszystko dziki lenistwu kompilatora! Czy wic nadal mona
podziela pogld, e ta cecha charakteru jest tylko przywar? :)
Funkcje operujce na obiektach klas szablonowych
Szablony funkcji s czsto przystosowane do manipulowanai obiektami klas
szablonowych - w zbliony sposb, w jaki czyni to zwyke funkcje z normalnymi klasami.
Popatrzmy na ten oto przykad funkcji Szukaj():
template <typename TYP> int Szukaj(const TArray<TYP>& aTablica,
TYP Szukany)
{
// przelatujemy po tablicy i porwnujemy elementy
for (unsigned i = 0; i < aTablica.Rozmiar(); ++i)
if (aTablica[i] == Szukany)
return i;
// jeli nic nie znajdziemy, zwracamy -1
return -1;
}
Sama jej tre do szczeglnie odkrywczych nie naley, a przeznaczenie jest, zdaje si,
oczywiste. Spjrzmy raczej na nagwek, bo to on sprawia, e mwimy o tym szablonie
w kategoriach wsppracy z szablonem klas TArray. Oto bowiem parametr szablonu TYP
Szablony 501
uywany jest jako parametr od TArray (midzy innymi). Dziki temu mamy wic ogln
funkcj do pracy z dowolnym rodzajem tablicy.
Taka wsppraca pomidzy szablonami klas i szablonami funkcji jest naturalna.
Gdziekolwiek bowiem umiecimy fraz template <...>, powoduje ona uniezalenienie
kodu od konkretnego typu danych. A jeli chcemy t niezaleno zachowa, to
nieuknione jest tworzenie kolejnych szablonw. W ten sposb skonstruowanych jest
mnstwo bibliotek jzyka C++, z Bibliotek Standardow na czele.
Specjalizacje szablonw klas
Teraz porozmawiamy sobie o definiowaniu specjalnych wersji szablonw klas dla
okrelonych parametrw (typw). Mechanizm ten dziaa do podobnie jak w przypadku
szablonw funkcji, wic nie powinno by z tym zbyt wielu problemw.
Specjalizowanie szablonu klasy
Specjalizacja szablonu klasy oznacza ni mniej wicej, jak tylko zdefiniowanie pewnej
szczeglnej wersji tego szablonu dla pewnego wyjtkowego typu (parametru szablonu).
Dodatkowo, istnieje moliwo specjlizacji pojedynczej metody; zajmiemy si pokrtce
oboma przypadkami.
Wasna klasa specjalizowana
Jako przykad na wasn, kompletn specjalizacj szablonu klasy posuymy si
oczywicie naszym szablonem tablicy jednowymiarowej - TArray. Dziaa on cakiem
dobrze w oglnej wersji, lecz przecie chcemy zdefiniowa jego specjalizacj. W tym
przypadku moe to by sensowne w odniesieniu do typu char. Tablica elementw tego
typu jest bowiem niczym innym, jak tylko acuchem znakw. Niestety, w obecnej formie
klasa TArray<char> nie moe by jako traktowana napis (obiekt std::string), bo dla
kompilatora nie ma teraz adnej praktycznej rnicy midzy wszystkimi typami tablic
TArray.
Aby to zmieni, musimy rzecz jasna wprowadzi swoj wasn specjalizacj TArray dla
parametru char. Klasa ta bdzie rnia si od wersji oglnej tym, i wewntrznym
mechanizmem przechowywania tablicy bdzie nie tablica dynamiczna typu char* (w
oglnoci: TYP*), lecz napis w postaci obiektu std::string. Pozwoli to na dodanie
operatora konwersji, aczkolwiek zmieni nieco kilka innych metod klasy. Spjrzmy wic na
t specjalizacj:
#include <string>
template<> class TArray<char>
{
static const unsigned DOMYSLNY_ROZMIAR = 5;
private:
// rzeczona tablica w postaci napisu std::string
std::string m_strTablica;
public:
// konstruktor
explicit TArray(unsigned uRozmiar = DOMYSLNY_ROZMIAR)
: m_strTablica(uRozmiar, '\0') { }
// (destruktor niepotrzebny)
//-------------------------------------------------------------
// (pomijam metody Pobierz() i Ustaw())
Zaawansowane C++ 502
unsigned Rozmiar() const
{ return static_cast<unsigned>(m_strTablica.length()); }
bool ZmienRozmiar(unsigned);
//-------------------------------------------------------------
// operator indeksowania
char& operator[](unsigned uIndeks) { return m_strTablica[i]; }
// operator rzutowania na typ std::string
operator std::string() const { return m_strTablica; }
};
C mona o niej powiedzie? Naturalnie, rozpoczynamy j, jak kad specjalizacj
szablonu, od frazy template<>. Nastpnie musimy jawnie poda parametry szablonu
(char), czyli nazw klasy szablonowej (TArray<char>). Wymg ten istnieje, bo definicja
tej klasy moe by zupenie rna od definicji oryginalnego szablonu!
Popatrzmy choby na nasz specjalizacj. Nie uywamy ju w niej tablicy dynamicznej
inicjowanej podczas wywoania konstruktora. Zamiast tego mamy obiekt klasy
std::string, ktremu w czasie tworzenia tablicy kaemy przechowywa podan liczb
znakw. Fakt, e sami nie alokujemy pamici sprawia te, e i sami nie musimy jej
zwalnia: napis m_strTablica usunie si sam - zatem destruktor jest ju niepotrzebny.
Poza tym nie ma raczej wielu niespodzianek. Do najciekawszych naley pewnie operator
konwersji na typ std::string - dziki niemu tablica TArray<char> moe by uywana
tam, gdzie konieczny jest acuch znakw C++. Dodanie tej niejawnej konwersji byo
gwnym powodem tworzenia wasnej specjalizacji; jak wida, zaoony cel zosta
osignity atwo i szybko.
Pozostaje jeszcze do zrobienia implementacja metody ZmienRozmiar(), ktr umiecimy
poza blokiem klasy. Kod wyglda moe tak:
bool TArray<char>::ZmienRozmiar(unsigned uNowyRozmiar)
{
try
{
// metoda resize() klasy std::string zmienia dugo napisu
m_strTablica.resize (uNowyRozmiar, '\0');
}
catch (std::length_error&)
{
// w razie niepowodzenia zmiany rozmiaru zwracamy false
return false;
}
// gdy wszystko si uda, zwracamy true
return true;
}
Od razu zwrmy uwag na brak klauzuli template<>. Nie ma jej, bowiem tutaj nie
mamy do czynienia ze specjalizacj szablonu ZmienRozmiar(). Metoda ta jest po prostu
zwyk funkcj klasy TArray<char> - podobnie byo zreszt w oryginalnym szablonie
TArray. Implementujemy j wic jako normaln metod. Nie ma tu zatem znaczenia
fakt, e metoda ta jest czci specjalizacji szablonu klasy. Najlepiej jest po prostu
zapamita, e dany szablon specjalizujemy raz i to wystarczy; gdybymy take tutaj
sprbowali doda template<>, to przecie byoby tak, jakbymy ponownie chcieli
Szablony 503
sprecyzowa fragment czego (metod), co ju zostao precyzyjnie okrelone jako cao
(klasa).
Co do treci metody, to uywamy tutaj funkcji std::string::resize() do zmiany
rozmiaru napisyu. Funkcja ta moe rzuci wyjtek w przypadku niepowodzenia. My ten
wyjtek przerabiamy na rezultat funkcji: false, jeli wystpi, i true, gdy wszystko si
uda.
Specjalizacja metody klasy
Przygldajc si uwanie specjalizacji TArray dla typu char mona odnie wraenie, e
przynajmniej czciowo zosta on stworzony poprzez skopiowanie skadowych z definicji
samego szablonu TArray. Przykadowo, funkcja dla operatora [] jest praktycznie
identyczna z t zamieszczon w oglnym szablonie (kwestia nazwy m_strTablica czy
m_pTablica jest przecie czysto symboliczna).
To moe nam si nieszczeglnie podoba, ale jest do przyjcia. Gorzej, jeli w klasie
specjalizowanej chcemy napisa nieco inn wersj tylko jednej metody z pierwotnego
szablonu. Czy wwczas jestemy skazani na specjalizowanie caej klasy oraz niewygodne
kopiowanie i bezsensowne zmiany prawie caego jej kodu?
Odpowied brzmi na szczcie Nie! Niech do jednej metody szablonu klasy nie
oznacza, e musimy obraa si na w szablon jako cao. Moliwe jest
specjalizowanie metod dla szablonw klas; wyjanijmy to na przykadzie.
Przypumy mianowicie, e zachciao nam si, aby tablica TArray zachowywaa si w
specjalny sposb w odniesieniu do elementw bdacych wskanikami typu int*. Ot
pragniemy, aby przy niszczeniu tablicy zwalniana bya take pami, do ktrej odnosz
si te wskaniki (elementy tablicy). Nie rozwodmy si nad tym, na ile jest to dorzeczne
programistycznie, lecz zastanwmy si raczej, jak to wykona. Chwila zastanowienia i
rozwizanie staje si jasne: potrzebujemy troch zmienionej wersji destruktora. Powinien
on jeszcze przed usuniciem samej tablicy zadba o zwolnienie pamici przynalenej
zawartym we wskanikom. Zmiana maa, lecz wana.
Musimy wic zdefiniowa now wersj destruktora dla klasy TArray<int*>. Nie jest to
specjalnie trudne:
template<> TArray<int*>::~TArray()
{
// przelatujemy po elementach tablicy (wskanikach) i kademu
// aplikujemy operator delete
for (unsigned i = 0; i < Rozmiar(); ++i)
delete m_pTablica[i];
// potem jak zwykle usuwamy te sam tablic
delete[] m_pTablica;
}
Jak to zwykle w specjalizacjach, zaczynamy od template<>. Dalej widzimy natomiast
normaln w zasadzie definicj destruktora. To, i jest ona specjalizacj metody dla
TArray z parametrem int* rozpoznajemy rzecz jasna po nagwku - a dokadniej, po
nazwie klasy: TArray<int*>.
Reszta nie jest chyba zaskoczeniem. W destruktorze TArray<int*> wpierw wic
przechodzimy po caej tablicy, stosujc operator delete dla kadego jej elementu
(wskanika). W ten sposb zwalniamy bloki pamici (zmienne dynamiczne), na ktre
pokazuj wskaniki. Z kolei po skoczonej robocie pozbywamy si take samej tablicy -
dokadnie tak, jak to czynilimy w szablonie TArray.
Zaawansowane C++ 504
Czciowa specjalizacja szablonu klasy
Pena specjalizacja szablonu oznacza zdefiniowanie klasy dla konkretnego, precyzyjnie
okrelonego zestawy argumentw. W naszym przypadku byo to dosowne podanie typ
elementw tablict TArray, np. char.
Czasem jednak taka precyzja nie jest podana. Niekiedy zdarza si, e wygodniej
byloby wprowadzi bardziej szczegow wersj szablonu, ktra nie operowaaby przy
tym konkretnymi typami. Wtedy wanie wykorzystujemy specjalizacj czciow
(ang. partial specialization). Zobaczymy to tradycyjnie na odpowiednim przykadzie.
Problem natury tablicowej
A zatem Nasz szablon tablicy TArray sprawdza si cakiem dobrze. Dotyczy to
szczeglnie prostych zastosowa, do ktrych zosta pierwotnie pomylany - jak na
przykad jednowymiarowa tablica liczb czy lista napisw. Idc dalej, atwo mona sobie
jednak wyobrazi bardziej zaawansowane wykorzystanie tego szablonu - w tym take
jako dwuwymiarowej tablicy tablic, np.:
TArray<TArray<int> > aInty2D;
Naturalnie chcielibymy, aby taka funkcjonalnoc bya nam dana niejako z urzdu, z
samej tylko definicji TArray. Zdawaoby si zreszt, e wszystko jest tutaj w porzadku i
e faktycznie moemy si posugiwa zmienn aInty2D jak tablic o dwch wymiarach.
Niestety, nie jest tak rowo; mamy tu przynajmniej dwa problemy.
Po pierwsze: w jaki sposb mielibymy ustali rozmiar(y) takiej tablicy? Typ zmiennej
aInty2D jest tu wprawdzie podwjny, ale przy jej tworzeniu nadal uywany jest
normalny konstruktor TArray, ktry jest jednoparametrowy. Moemy wic poda
wycznie jeden wymiar tablicy, za drugi zawsze musiaby by rwny wartoci
domylnej!
Oprcz tego oczywistego bdu (cakowicie wykluczajcego uycie tablicy) mamy jeszcze
jeden mankament. Mianowicie, zawarto tablicy nie jest rozmieszczona w pamici w
postaci jednego bloku, jak to czyni kompilator w wypadku statycznych tablic. Zamiast
tego kady jej wiersz (podtablica) jest umieszczony w innym miejscu, co przy wikszej
ich liczbie, rozmiarach i czstym dostpie bdzie ujemnie odbijao si na efektywnoci
kodu.
Rozwizanie: przypadek szczeglniejszy, ale nie za bardzo
Co mona na to poradzi? Rozwizaniem jest specjalne potraktowanie klasy
TArray<TArray<typ_elementu> > i zdefiniowanie jej odmiennej postaci - nieco innej ni
wyjciowy szablon TArray. Przedtem jednak zwrmy uwag, i nie moemy tutaj
zastosowa cakowitej specjalizacji tego szablonu, bowiem typ_elementu nadal jest tu
parametrem o dowolnej wartoci (typie).
Jak si pewnie domylasz, trzeba tu zastosowa specjalizacj czciow. Bdzie ona
traktowaa zagniedone szablony TArray w specjalny sposb, zachowujc jednak
moliwo dowolnego ustalania typu elementw tablicy. Popatrzmy wic na definicj tej
specjalizacji:
template <typename TYP> class TArray<TArray<TYP> >
{
static const unsigned DOMYSLNY_ROZMIAR = 5;
private:
// wskanik na tablic
TYP* m_pTablica;
// wymiary tablicy
unsigned m_uRozmiarX;
Szablony 505
unsigned m_uRozmiarY;
public:
// konstruktor i destruktor
explicit TArray(unsigned uRozmiarX = DOMYSLNY_ROZMIAR,
unsigned uRozmiarY = DOMYSLNY_ROZMIAR)
: m_uRozmiarX(uRozmiarX), m_uRozmiar(uRozmiarY),
m_pTablica(new TYP [uRozmiarX * uRozmiarY]) { }
~TArray() { delete[] m_pTablica; }
//-------------------------------------------------------------
// metody zwracajce wymiary tablicy
unsigned RozmiarX() const { return m_uRozmiarX; }
unsigned RozmiarY() const { return m_uRozmiarY; }
//-------------------------------------------------------------
// operator () do wybierania elementw tablicy
TYP& operator()(unsigned uX, unsigned uY)
{ return m_pTablica[uY * m_uRozmiarX + uX]; }
// (pomijam konstruktor kopiujcy i operator przypisania}
};
Tak naprawd to w opisywanej sytuacji specjalizacja czciowa niekoniecznie moe by
uznawana za najlepsze rozwizanie. Do logiczne jest bowiem zdefiniowanie sobie
zupenie nowego szablonu, np. TArray2D i wykorzystywanie go zamiast misternej
konstrukcji TArray<TArray<...> >. Poniewa jednak masz tutaj przede wszystkim
pozna zagadnienie specjalizacji czciowej, wycz na chwil swj nazbyt czuy
wykrywacz naciganych rozwiza i w spokoju kontynuuj lektur :D
Rozpoczyna si ona od sekwencji template <typename TYP> (a nie template<>), co
moe budzi zaskoczenie. W rzeczywistoci jest to logiczne i niezbdne: to prawda, e
mamy do czynienia ze specjalizacj szablonu, jednak jest to specjalizacja czciowa,
zatem nie okrelamy explicit wszystkich jego parametrw. Nadal wic posugujemy si
faktycznym szablonem - choby w tym sensie, e typ elementw tablicy pozostaje
nieznany z gry i musi podlega parametryzacji jako TYP. Klauzula template <typename
TYP> jest zatem niezbdna - podobnie zreszt jak we wszystkich przypadkach, gdy
tworzymy kod niezaleny od konkretnego typu danych.
Tutaj klauzula ta wyglda tak samo, jak w oryginalnym szablnoie TArray. Warto jednak
wiedzie, e nie musi wcale tak by. Przykadowo, jeli specjalizowalibymy szablon o
dwch parametrach, wwczas fraza template <...> mogaby zawiera tylko jeden
parametr. Drugi musiaby by wtedy narzucony odgrnie w specjalizacji.
Kompilator wie jednak, e nie jest to taki zwyczajny szablon podstawowy. Dalej bowiem
okrelamy dokadnie, o jakie przypadki uycia TArray nam chodzi. S to wic te
sytuacje, gdy klasa parametryzowana nazw TYP (TArray<TYP>) sama staje si
parametrem szablonu TArray, tworzc swego rodzaju zagniedenie (tablic tablic) -
TArray<TArray<TYP> >. O tym wiadczy pierwsza linijka naszej definicji, czyli:
template <typename TYP> class TArray<TArray<TYP> >
Sam blok klasy wynika bezporednio z tego, e programujemy tablic dwuwymiarow
zamiast jednowymiarowej. Mamy wic dwa pola okrelajce jej rozmiar - liczb wierszy i
ilo kolumn. Wymiary te podajemy w nowym, dwuparametrowym konstruktorze:
Zaawansowane C++ 506
explicit TArray(unsigned uRozmiarX = DOMYSLNY_ROZMIAR,
unsigned uRozmiarY = DOMYSLNY_ROZMIAR)
: m_uRozmiarX(uRozmiarX), m_uRozmiar(uRozmiarY),
m_pTablica(new TYP [uRozmiarX * uRozmiarY]) { }
Ten za dokonuje alokacji pojedynczego bloku pamici na ca tablic - a o to nam
przecie chodzio. Wielko tego bloku jest rzecz jasna na tyle dua, aby pomieci
wszystkie elementy - rwna si ona iloczynowi wymiarw tablicy (bo np. tablica 47 ma
w sumie 28 elementw, itp.).
Niestety, fakt i jest to tablica dwuwymiarowa, uniemoliwia przecienie w prosty
sposb operatora [] celem uzyskania dostpu do poszczeglnych elementw tablicy.
Zamiast tego stosujemy wic inny rodzaj nawiasw - okrge. Te bowiem pozwalaj na
podanie dowolnej liczby argumentw (indeksw); my potrzebujemy naturalnie dwch:
TYP& operator()(unsigned uX, unsigned uY)
{ return m_pTablica[uY * m_uRozmiarX + uX]; }
Uywamy ich potem, aby zwrci element o danych indeksach. Wewntrzna
m_pTablica jest aczkolwiek ciga i jednowymiarowa (bo ma zajmowa pojedynczy blok
pamici), dlatego konieczne jest przeliczenie indeksw. Zajmuje si tym formuka uY *
m_uRozmiar + uX, sprawiajc jednoczenie, e elementy tablicy s ukadane w pamici
wierszami. Przypadkowo zgadza si to ze sposobem, jaki stosuje kompilator jzyka
C++.
Na koniec popatrzmy jeszcze na sposb uycia tej (czciowo) specjalizowanej wersji
szablonu TArray. Oto przykad kodu, ktry z niej korzysta:
TArray<TArray<double> > aMacierz4x4(4, 4);
// dostp do elementw tablicy
for (unsigned i = 0; i < aMacierz4x4.RozmiarX(); ++i)
for (unsigned j = 0; j < aMacierz4x4.RozmiarY(); ++j)
aMacierz4x4(i, j) = i + j;
Tak wic dziki specjalizacji czciowej klasa TArray<TArray<double> > i inne tego
rodzaju mog dziaa poprawnie, co nie bylo moliwe, gdy obecna bya jedynie
podstawowa wersja szablonu TArray.
Domylne parametry szablonu klasy
Szablon klasy ma swoj list parametrw, z ktrych kady moe mie swoj warto
domyln. Dziaa to w analogiczny sposb, jak argumenty domylne wywoa funkcji.
Popatrzmy wic na t technik.
Typowy typ
Zanim jednak popatrzymy na sam technik, popatrzmy na taki oto szablon:
// para
template <typename TYP1, typename TYP2> struct TPair
{
// elementy pary
TYP1 Pierwszy;
TYP2 Drugi;
//-------------------------------------------------------------------
// konstruktor
TPair(const TYP1& e1, const TYP2& e2) : Pierwszy(e1), Drugi(e2) { }
Szablony 507
};
Reprezentuje on par wartoci rnych typw. Taka struktura moe si wydawa lekko
dziwaczna, ale zapewniam, e znajduje ona swoje zastosowania w rznych
nieprzewidzianych momentach :) Zreszt nie o zastosowania tutaj chodzi, lecz o
parametey szablonu.
A mamy tutaj dwa takie parametry: typy obu obiektw. Uycie naszej klasy wyglda
wic moe chociaby tak:
TPair<int, int> Dzielnik(42, 84);
TPair<std::string, int> Slownie("dwanacie", 12);
TPair<float, int> Polowa(2.5f, 5);
Przypumy teraz, e w naszym programie czsto zdarza si nam, i jeden z obiektw w
parze naley do jakiego znanego z gry typu. W kodzie powyej na przykad kada z
tych par ma jeden element typu int.
Chcc zaoszczdzi sobie koniecznoci pisania tego podczas deklarowania zmiennych,
moemy uczyni int argumentem domylnym:
template <typename TYP1, typename TYP2 = int> struct TPair
{
// ...
};
Piszc w ten sposb sprawiamy, e w razie niepodania wartoci dla drugiego parametru
szablonu, ma on oznacza typ int:
TPair<CFoo> Wielkosc(CFoo(), sizeof(CFoo)); // TPair<CFoo, int>
TPair<double> Pierwiastek(sqrt(2), 2); // TPair<double, int>
TPair<int> DwaRaz(12, 6); // TPair<int, int>
Okrelajc parametr domylny pamitajmy jednak, e:
Parametr szablonu moe mie warto domyln tylko wtedy, gdy znajduje si na
kocu listy lub gdy wszystkie parametry za nim te maj warto domyln.
Niepoprawny jest zatem szablon:
template <typename TYP1 = int, typename TYP2> struct TPair; // LE!
Nic aczkolwiek nie stoi na przeszkodzie, aby poda wartoci domylne dla wszystkich
parametrw:
template <typename TYP1 = std::string, typename TYP2 = int>
struct TPair; // OK
Uywajc takiego szablonu, nie musimy ju podawa adnych typw, aczkolwiek naley
zachowa nawiasy ktowe:
TPair<> Opcja("Ilo plikw", 200); // TPair<std::string, int>
Obecnie domylne argumenty mona podawa wycznie dla szablonw klas. Jest to
jednak pozostao po wczesnych wersjach C++, niemajca adnego uzasadnienia, wic
jest cakiem prawdopodobne, e ograniczenie to zostanie wkrtce usunite ze Standardu.
Co wicej, sporo kompilatorw ju teraz pozwala na podawanie domylnych argumentw
szablonw funkcji.
Zaawansowane C++ 508
Skorzystanie z poprzedniego parametru
Dobierajc parametr domylny szablonu, moemy te skorzysta z poprzedniego. Oto
przykad dla naszej pary:
template <typename TYP1, typename TYP2 = TYP1> struct TPair;
Przy takim postawieniu sprawy i podaniu jednego parametru szablonu bdziemy mieli
pary identycznych obiektw:
TPair<int> DwaDo(8, 256);
TPair<std::string> Tlumaczenie("tablica", "array");
TPair<double> DwieWazneStale(3.14, 2.71);
Mona jeszcze zauway, e identyczny efekt osignlibymy przy pomocy czciowej
specjalizacji szablonu TPair dla tych samych argumentw:
template <typename TYP> struct TPair<TYP, TYP>
{
// elementy pary
TYP Pierwszy;
TYP Drugi;
//-------------------------------------------------------------------
// konstruktor
TPair(const TYP& e1, const TYP& e2) : Pierwszy(e1), Drugi(e2) { }
};
Domylne argumenty maj jednak t oczywist zalet, e nie zmuszaj do praktycznego
dublowania definicji klasy (tak jak powyej). W tym konkretnym przypadku s one
znacznie lepszym wyborem. Jeeli jednak posta szablonu dla pewnej klasy parametrw
ma si znaczco rni, wwczas dosownie napisana specjalizacja jest najczciej
konieczna.
***
Na tym koczymy prezentacj szablonw funkcji oraz klas. To aczkolwiek nie jest jeszcze
koniec naszych zmaga z szablonami w ogle. Jest bowiem jeszcze kilka rzeczy
oglniejszych, o ktrych naley koniecznie wspomnie. Przejdmy wic do kolejnego
podrozdziau na temat szablonw.
Wicej informacji
Po zasadniczym wprowadzeniu w tematyk szablonw zajmiemy si nieco szczegowiej
kilkoma ich aspektami. Najpierw wic przestudiujemy parametry szablonw, potem za
zwrcimy uwag na pewne problemy, jakie moga wynikn podczas stosowania tego
elementu jzyka. Najwicej uwagi powicimy tutaj sprawie organizacji kodu szablonw
w plikach nagwkowych i moduach, gdy jest to jedna z kluczowych kwestii.
Zatem poznajmy szablony troch bliej.
Parametry szablonw
Dowiedziae si na samym pocztku, e kady szablon rozpoczyna si od obowizkowej
frazy w postaci:
Szablony 509
[export] template <parametry>
O nieobowizkowym sowie kluczowym export powiemy w nastpnej sekcji, w paragrafie
omawiajcym tzw. model separacji.
Nazywamy j klauzul parametryzacji (ang. parametrization clause). Peni ona w
kodzie dwojak funkcj:
informuje ona kompilator, e nastpujcy dalej kod jest szablonem. Dziki temu
kompilator wie, e nie powinien dla przeprowadza normalnej kompilacji, lecz
potraktowa w sposb specjalny - czyli podda konkretyzacji
klauzula zawiera te deklaracje parametrw szablonu, ktre s w nim uywane
Wanie tymi deklaracjami oraz rodzajami i uyciem parametrw szablonu zajmiemy si
obecnie. Na pocztek warto wic wiedzie, e parametry szablonw dzielimy na trzy
rodzaje:
parametry bdce typami
parametry bdce staymi znanymi w czasie kompilacji (tzw. parametry
pozatypowe)
szablony parametrw
Dotychczas w naszych szablonach niepodzielnie krloway parametry bdce typami.
Nadal bowiem s to najczciej wykorzystywane parametry szablonw; dotd mwi si
nawet, e szablony i kod niezaleny od typu danych to jedno i to samo.
My jednak nie moemy pozwoli sobie na ignoracj w zakresie ich parametrw. Dlatego
te teraz omwimy dokadnie wszystkie rodzaje parametrw szablonw.
Typy
Parametry szablonw bdce typami stanowi najwiksz si szablonw, przyczyn ich
powstania, niespotykanej popularnoci i przydatnoci. Nic wic dziwnego, e pierwsze
poznane przez nas przykady szablonw korzystay wanie z parametryzowania typw.
Nabrae wic cakiem sporej wprawy w ich stosowaniu, a teraz poznasz kryjc si za
tym fasad teorii ;)
Przypominamy banalny przykad
W tym celu przywoajmy pierwszy przykad szablonu, z jakim mielimy do czynienia, czyli
szablon funkcji max():
template <typename T> T max(T a, T b)
{
return (a > b ? a : b);
}
Ma on jeden parametr, bdcy typem; parametr ten nosi nazw T. Jest to zwyczajowa
ju nazwa dla takich parametrw szablonu, ktr mona spotka niezwykle czsto.
Zgodnie z t konwencj, nazw T nadaje si parametrowi bdcemu typem, jeli jest on
jednoczenie jedynym parametrem szablonu i w zwizku z tym peni jak szczegln
rol. Moe to by np. typ elementw tablicy czy, tak jak tutaj, typ parametrw funkcji i
zwracanej przez ni wartoci.
Nazwa T jest tu wic symbolem zastpczym dla waciwego typu porwnywanych
wartoci. Jeeli pojcie to sprawia ci trudno, wyobra sobie, e dziaa ono podobnie jak
alias typedef. Mona wic przyj, e kompilator, stosujc funkcj w konkretnym
przypadku, definiuje T jako alias na waciwy typ. Przykadowo, specjalizacj max<int>
mona traktowa jako kod:
Zaawansowane C++ 510
typedef int T;
T max (T a, T b) { return (a > b ? a : b); }
Naturalnie, w rzeczywistoci generowana jest po prostu funkcja:
int max<int>(int a, int b);
Niemniej powyszy sposb moe ci z pocztku pomc, jeli dotd nie rozumiae idei
parametru szablonu bdcego typem.
class zamiast typename
Parametr szablonu bdcy typem oznaczalimy dotd za pomoc sowa kluczowego
typename. Okazuje sie, e mona to take robi poprzez swko class:
template <class T> T max(T a, T b);
Nie oznacza to bynajmniej, e podany parametr szablonu moe by wycznie klas
zdefiniowan przez uytkownika
125
. Przeciwnie, ot:
Slowa class i typename w s synonimami w deklaracjach parametrw szablonu
bdcych typami.
Po co zatem istniej dwa takie sowa? Jest to spowodowane tym, i pierwotnie jedynym
sposobem na deklarowanie parametrw szablonu byo class. typename wprowadzono do
jzyka pniej, i to w cakiem innym przeznaczeniu (o ktrym te sobie powiemy). Przy
okazji aczkolwiek pozwolono na uycie tego nowego slowa w deklaracjach parametrw
szablonw, jako e znacznie lepiej pasuje tutaj ni class. Dlatego te mamy ostatecznie
dwa sposoby na zrobienie tego samego.
Mona z tego wycign pewn korzy. Wprawdzie dla kompilatora nie ma znaczenia,
czy do deklaracji parametrw uywamy class czy typename, lecz nasz wybr moe mie
przecie znaczenie dla nas. Logiczne jest mianowicie uywanie class wycznie tam,
gdzie faktycznie spodziewamy si, e przekazanym typem bdzie klasa (bo np.
wywoujemy jej metody). W pozostaych przypadkach, gdy typ moe by absolutnie
dowolny (jak choby w uprzednich szablonach max() czy TArray), rozsdne jest
stosowanie typename.
Naturalnie, to tylko sugestia, bo jak mwiem ju, kompilatorowi jest w tej kwestii
wszystko jedno.
Stae
Cenn waciwoci szablonw jest moliwo uycia w nich innego rodzaju parametrw
ni tylko typy. S to tak zwane parametry pozatypowe (ang. non-type parameters), a
dokadniej mwic: stae.
Uycie parametrw pozatypowych
Ich wykorzystanie najlepiej bdzie zobaczy na paru rozsdnych przykadach.
125
Czyli typem zdefiniowanym poprzez struct, union lub class.
Szablony 511
Przykad szablonu klasy
W poprzednim paragrafie zdefiniowalimy sobie szablon klasy TArray. Suy on jako
jednowymiarowa tablica dynamiczna, ktrej rozmiar podawalimy przy tworzeniu i
ewentualnie zmienialimy w trakcie korzystania z obiektu.
Mona sobie jeszcze wyobrazi podobny szablon dla tablicy statycznej, ktrej rozmiar jest
znany podczas kompilacji. Oto propozycja szablonu TStaticArray:
template <typename T, unsigned N> class TStaticArray
{
private:
// tablica
T m_aTablica[N];
public:
// rozmiar tablicy jako staa
static const unsigned ROZMIAR = N;
//-------------------------------------------------------------
// operator indeksowania
T& operator[](unsigned uIndeks)
{ return m_aTablica[uIndeks]; }
// (itp.)
};
Jak susznie zauwaye, szablon ten zawiera dwa parametry. Pierwszy z nich to typ
elementw tablicy, deklarowany w znany sposb poprzez typename. Natomiast drugi
parametr jest wanie przedmiotem naszego zainteresowania. Stosujemy w nim typ
unsigned, wobec czego bdzie on sta tego wanie typu.
Popatrzmy najlepiej na sposb uycia tego szablonu:
TStaticArray<int, 10> a10Intow; // 10-elementowa tablica typu int
TStaticArray<float, 20> a20Floatow; // 20 liczb typu float
TStaticArray<
TStaticArray<double, 5>,
8> a8x5Double; // tablica 85 liczb typu double
Podobnie jak w przypadku parametrw bdcych typami moesz sobie wyobrazi, e
kompilator konkretyzuje szablon, definiujc warto N jako sta. Klasa
TStaticArray<float, 10> odpowiada wic mniej wicej zapisowi w takiej postaci:
typedef float T;
const unsigned N = 10;
class TStaticArray
{
private:
T m_aTablica[N];
// ...
};
Wynika z niego przede wszystkim to, i:
Parametry pozatypowe szablonw s traktowane wewntrz nich jako stae.
Zaawansowane C++ 512
Oznacza to przede wszystkim, e musz by one wywoywane z wartocami, ktre s
obliczalne podczas kompilacji. Wszystkie pokazane powyej konkretyzacje s wic
poprawne, bo 10, 20, 5 i 8 s rzecz jasna staymi dosownymi, a wic znanymi w czasie
kompilacji. Nie byoby natomiast dozwolone uycie szablonu jako TStaticArray<typ,
zmienna>, gdzie zmienna niezadeklarowana zostaa z przydomkiem const.
Przykad szablonu funkcji
Gdy mamy ju zdefiniowany nowy szablon tablicy, moemy sprbowa stworzy dla niego
odpowiadajc wersj funkcji Szukaj(). Naturalnie, bdzie to rwnie szablon:
template <typename T, unsigned N>
int Szukaj(const TStaticArray<T, N>& aTablica, T Szukany)
{
// przegld tablicy
for (unsigned i = 0; i < N; ++i)
if (aTablica[i] == Szukany)
return i;
// -1, gdy nie znaleziono
return -1;
}
Wida tutaj, e parametr pozatypowy moe by z rwnym powodzeniem uyty zarwno w
nagwku funkcji (typ const TStaticArray<T, N>&), jak i w jej wntrzu (warunek
zakoczenia ptli for).
Dwie wartoci, dwa rne typy
Wyobramy sobie, e mamy dwie tablice tego samego typu, ale o rnych rozmiarach:
TStaticArray<int, 20> a20Intow;
TStaticArray<int, 10> a10Intow;
Sprbujmy teraz przypisa t mniejsz do wikszej, w ten oto sposb:
a20Intow = a10Intow; // hmm...
Teoretycznie powinno by to jak najbardziej moliwe. Pierwszym 10 elementw tablicy
a20Intow mogoby by przecie zastpione zawartoci zmiennej a10Intow. Nie ma
zatem przeciwwskaza.
Niestety, kompilator odrzuci taki kod, mwic, i nie znalaz adnego pasujcego
operatora przypisania ani niejawnej konwersji. I bdzie to szczera prawda! Musimy
bowiem pamita, e:
Szablony klas konkretyzowane innym zestawem parametrw s zupenie
odmiennymi typami.
Nic wic dziwnego, e TStaticArray<int, 10> i TStaticArray<int, 20> s traktowane
jako odrbne klasy, niezwizane ze sob (obie te nazwy, wraz z zawartoci nawiasw
ktowych, s bowiem nazwami typw, o czym przypominam po raz ktry). W takim
wypadku domylnie generowany operator przypisania zawodzi. Warto wic pamita o
powyszej zasadzie.
No ale skoro mamy ju taki problem, to przydaoby si go rozwiza. Odpowiednim
wyjciem jest wasny operator przypisania zdefiniowany jako szablon skadowej:
template <typename T, unsigned N> class TStaticArray
{
Szablony 513
// ...
public:
// operator przypisania jednej tablicy do drugiej
template <typename T2, unsigned N2>
TStaticArray&
operator=(const TStaticArray<T2, N2>& aTablica)
{
// kontrola przypisania zwrotnego
if (&aTablica != this)
{
// sprawdzenie rozmiarw
if (N2 > N)
throw "Za duza tablica";
// przepisanie tablicy
for (unsigned i = 0; i < N2; ++i)
(*this)[i] = aTablica[i];
}
return *this;
}
};
Moe i wyglda on nieco makabrycznie, ale w gruncie rzeczy dziaa na identycznej
zasadzie jak kady rozsdny operator przypisania. Zauwamy, e parametryzacji podlega
w nim nie tylko rozmiar rdwej tablicy (N2), ale te typ jej elementw (T2). To, czy
przypisanie faktycznie jest moliwe, zaley od tego, czy powiedzie si kompilacja
instrukcji:
(*this)[i] = aTablica[i];
A tak bdzie oczywicie tylko wtedy, gdy istnieje niejawna konwersja z typu T2 do T.
Ograniczenia dla parametrw pozatypowych
Pozatypowe parametry szablonw w przeciwiestwie do parametrw funkcji nie mog by
wywoywane z dowolnymi wartociami. Typami tyche parametrw mog by bowiem
tylko:
typy liczbowe, czyli int i jego pochodne (signed lub unsigned)
typy wyliczeniowe (definiowane poprzed enum)
wskaniki do obiektw i funkcji globalnych
wskaniki do skadowych klas
Lista ta jest do krtka i moe si wydawa nazbyt restrykcyjna. Tak jednak nie jest.
Gwnie ze wzgldu na sposb dziaania szablonw ich parametry pozatypowe s
ograniczone tylko do takich rodzajw.
Przyjrzyjmy si jeszcze kilku szczeglnym przypadkom tych ogranicze.
Wskaniki jako parametry szablonu
Nie ma adnych przeciwskaza, aby deklarowa szablony z parametrami bdcymi
typami wskanikowymi. Wyglda to na przykad tak:
template <int* P> class TClass
{
// ...
};
Zaawansowane C++ 514
Gorzej wyglda sprawa z uyciem takiego szablonu. Ot nie moemy przekaza mu
wskanika ani na obiekt chwilowy, ani na obiekt lokalny, ani nawet na obiekt o zasigu
moduowym. Nie jest wic poprawny np. taki kod:
int nZmienna;
TClass<&nZmienna> Obiekt; // LE! Wskanik na obiekt lokalny
Wyjanienie jest tu proste. Wszystkie takie obiekty maj po prostu zbyt may zakres,
ktry nie pokrywa si z widocznoci konkretyzacji szablonu. Aby tak byo, obiekt, na
ktry wskanik podajemy, musiaby by globalny (czony zewntrznie):
extern int g_nZmienna = 42;
// ...
TClass<&g_Zmienna> Cos; // OK
Z identycznych powodw nie mona do szablonw przekazywa acuchw znakw:
template <const char[] S> class TStringer { /* ... */ };
TStringer<"Hmm..."> Napisowiec; // NIE!
acuch "Hmm..." jest tu bowiem obiektem chwilowym, zatem szybko przestaby istnie.
Typ TStringer<"Hmm..."> musiaby natomiast egzystowa i by potencjalnie dostpnym
w caym programie. To oczywicie wzajemnie si wyklucza.
Inne restrykcje
Oprcz powyszych obostrze s jeszcze dwa inne.
Po pierwsze, w charakterze parametrw szablonu nie mona uywa obiektw
wasnych klas. Ponisze szablony s wic niepoprawne:
template <CFoo F> class TMetaFoo { /* ... */ };
template <std::string S> class TStringTemplate { /* ... */ };
Poza tym, w charakterze parametrw pozatypowych teoretrycznie niedozwolone s
wartoci zmiennoprzecinkowe:
template <float F> class TCalc { /* ... */ };
Mwi teoretycznie, gdy wiele kompilatorw pozwala na ich uycie. Nie ma bowiem ku
temu adnych technicznych przeciwwskaza (w odrnieniu od pozostaych ogranicze
parametrw pozatypowych). Niemniej, w Standardzie C++ nadal zakorzenione jest to
przestarzae ustalenie. Zapewne jednak tylko kwesti czasu jest jego usunicie.
Szablony parametrw
Ostatnim rodzajem parametrw s tzw. szablony parametrw szablonw
(ang. template templates parameters). Pod t dziwnie brzmic nazw kryje si
moliwo przekazania jako parametru nie konkretnego typu, ale uprzednio
zdefiniowanego szablonu. Poniewa zapewnie nie brzmi to zbyt jasno, najrozsdniej
bdzie doj do sedna sprawy przy pomocy odpowiedniego przykadu.
Idc za potrzeb
A wic Swego czasu stworzylimy sobie szablon oglnej klasy TArray. Okazuje si
jednak, e niekiedy moe by on niewystarczajcy. Chocia dobrze nadaje si do samej
czynnoci przechowywania wartoci, nie pomylelimy o adnych mechanizmach
operowania na tyche wartociach.
Szablony 515
Z drugiej strony, nie ma sensu zmiany dobrze dziaajcego kodu w co, co nie zawsze
bdzie nam przyadtne. Takie czynnoci jak dodawnie, odejmowanie czy mnoenie tablic
maj bowiem sens tylko w przypadku wektorw liczb. Lepiej wic zdefiniowa sobie nowy
szablon do takich celw:
template <typename T> class TNumericArray
{
private:
// wewntrzna tablica
TArray<T> m_aTablica;
public:
// ...
// jakie operatory...
// (np. indeksowania)
TNumericArray operator+(const TNumericArray& aTablica)
{
TNumericArray Wynik(*this);
for (unsigned i = 0; i < Wynik.Rozmiar(); ++i)
Wynik[i] += aTablica[i];
return Wynik;
}
// (itp.)
};
W sumie nic specjalnego nie moemy powiedzie o tym szablonie klasy TNumericArray.
Jak si pewnie domylasz, to si za chwil zmieni :)
Dodatkowy parametr: typ wewntrznej tablicy
Moe si okaza, e w naszym programie zmuszeni jestemy do operowania zarwno
wielkimi tablicami, jak i mniejszymi. W wypadku tych drugim wewntrzny szablon TArray
sucy do ich przechowywania pewnie zda egzamin, ale gdy liczba elementw ronie,
mog by konieczne bardziej wyrafinowane techniki zarzdzania pamici.
Aby sprosta temu wymaganiu, rozsdnie byoby umoliwi wybr typu wewntrznej
tablicy dla szablonu TNumericArray:
template <typename T, typename TAB = TArray<T> >
class TNumericArray
{
private:
TAB m_aTablica;
// ...
};
Domylnie byby to nadal szablon TArray, niemniej przy takim szablonie TNumericArray
monaby w miar atwo deklarowa zarwno due, jak i mae tablice:
TNumericArray<int> aMalaTablica(50);
TNumericArray<float, TOptimizedArray<float> > aDuzaTablica(1000);
TNumericArray<double, TSuperFastArray<double> > aGigaTablica(250000);
Zaawansowane C++ 516
W tym przykadzie zakadamy oczywicie, e TOptimizedArray i TSuperFastArray s
jakimi uprzednio zdefiniowanymi szablonami tablic efektywniejszych od TArray. W
uzasadnionych przypadkach duej liczby elementw ich uycie jest wic pewnie
podane, co te czynimy.
Drobna niedogodno
Powysze rozwizanie ma jednak pewien drobny mankament skadniowy. Nietrudno
mianowicie zauway, e dwa razy piszemy w nim typ elementw tablic - float i double.
Pierwszy raz jest on podawany szablonowi TNumericArray, a drugi raz - szablonowi
wewntrznej tablicy.
W sumie powoduje to zbytni rozwleko nazwy caego typu TNumericArray<...>, a na
dodatek ujawnia osawiony problem nawiasw ostrych. Wydaje si przy tym, e
informacj o typie podajemy o jeden raz za duo; w kocu zamiast deklaracji:
TNumericArray<float, TOptimizedArray<float> > aDuzaTablica(1000);
TNumericArray<double, TSuperFastArray<double> > aGigaTablica(250000);
rwnie dobrze mogoby si sprawdza co w tym rodzaju:
TNumericArray<float, TOptimizedArray> aDuzaTablica(1000);
TNumericArray<double, TSuperFastArray> aGigaTablica(250000);
Problem jednak w tym, e parametry szablonu TNumericArray - TOptimizedArray i
TSuperFastArray nie s zwykymi typami danych (klasami), wic nie pasuj do
deklaracji typename TAB. One same s szablonami klas, zdefiniowanymi zapewne kodem
podobnym do tego:
template <typename T> class TOptimizedArray { /* ... */ };
template <typename T> class TSuperFastArray { /* ... */ };
Mona wic powiedzie, e wystpuje to swoista niezgodno typw midzy pojciami
typ i szablon klasy. Czy zatem nasz pomys skrcenia sobie zapisu trzeba odrzuci?
Deklarowanie szablonowych parametrw szablonw
Bynajmniej. Midzy innymi na takie okazje cakiem niedawno jzyk C++ wyposaono w
moliwo deklarowania szczeglnego rodzaju parametrw szablonu. Te specjalne
parametry charakteryzuj si tym, e s nazwami zastpczymi dla szablonw klas.
Jako takie wymagaj wic podania nie konkretnego typu danych, lecz jego uoglnienia -
szablonu klasy.
Dobr, nieszablonow analogi dla tej niecodziennej konstrukcji jest sytuacja, gdy
funkcja przyjmuje jako parametr inn funkcj poprzez wskanik. W mniej wicej zbliony
koncepcyjnie sposb dziaaj szablonowe parametry szablonw.
Oto jak deklaruje si i uywa tych specjalnych parametrw:
template <typename T, template <typename> class TAB = TArray>
class TNumericArray
{
private:
TAB<T> m_aTablica;
// ...
};
Szablony 517
Posugujemy si tu dwa razy sowem kluczowym template. Pierwsze uycie jest ju
powszechnie znane; drugie wystpuje w licie parametrw szablonu TNumericArray i o
nie nam teraz chodzi. Przy jego pomocy deklarujemy bowiem szablon parametru.
Skadnia:
template <typename> class TAB
oznacza tutaj, e do parametru TAB pasuj wszystkie szablony klas (template <...>
class), ktre maj dokadnie jeden parametr bdcy typem (typename
126
). W przypadku
niepodania adnego szablonu, zostanie wykorzystany domylny - TArray.
Teraz, gdy nazwa TAB jest ju nie klas, lecz jej szablonem, uywamy jej tak jak
szablonu. Deklaracja pola wewntrznej tablicy wyglda wic nastpujco:
TAB<T> m_aTablica;
Jako parametr dla TAB podajemy T, czyli pierwszy parametr naszego szablonu
TNumericArray. W sumie jednak monaby uy dowolnego typu (take podanego
dosownie, np. int), bo TAB zachowuje si tutaj tak samo, jak penoprawny szablon
klasy.
Naturalnie, teraz poprawne staj si propozycje deklaracji zmiennych z poprzedniego
akapitu:
TNumericArray<float, TOptimizedArray> aDuzaTablica(1000);
TNumericArray<double, TSuperFastArray> aGigaTablica(250000);
Na ile przydatne s szablony parametrw szablonw (zwane te czasem
metaszablonami - ang. metatemplates) musisz si waciwie przekona sam. Jest to
jedna z tych cech jzyka, dla ktrych trudno od razu znale jakie oszaamiajce
zastosowanie, ale jednoczenie moe okaza si przydatna w pewnych szczeglnych
sytuacjach.
Problemy z szablonami
Szablony s rzeczywicie jednym z najwikszych osigni jzyka C++. Jednak, jak to
jest z wikszoci zaawansowanych technik, ich stosowanie moe za soba pociga
pewne problemy. Nie, nie chodzi mi tu wcale o to, e szablony s trudne do nauczenia,
cho pewnie masz takie nieodparte wraenie ;) Chciabym raczej porozmawia o kilku
puapkach czyhajcych na programist (szczeglnie pocztkujcego), ktry zechce
uywa szablonw. Dziki temu by moe atwiej unikniesz mniej lub bardziej powanych
problemw z tymi konstrukcjami jzykowymi.
Zobaczmy wic, co moe stan nam na drodze
Uatwienia dla kompilatora
ledzc opis czynnoci, jakie wykonuje kompilator w zwizku z szablonami, mona
zauway, e zmuszony jest do icie ekwilibrystycznych wygibasw. To zrozumiae, jeli
przypomnimy sobie, e kontrola typw jest w C++ jednym z filarw programowania, za
szablony czciowo su wanie do jej obejcia.
126
Nie podajemy nazwy parametru szablonu TAB, bo nie ma takiej potrzeby. Nazwa ta nie jest nam po prostu
do niczego potrzebna.
Zaawansowane C++ 518
Na kompilatorze spoczywa mnstwo trudnych zda, jeli chodzi o kod wykorzystujcy
szablony. Dlatego te niekiedy potrzebuje on wsparcia ze strony programisty, ktre
uatwioby mu intepretacj kodu rdowego. O takich wanie uatwieniach dla
kompilatora traktuje niniejszy paragraf.
Nawiasy ostre
Niejednego nowicjusza w uywaniu szablonw zjad smok o nazwie problem nawiasw
ostrych. Nietrudno przecie wyprodukowa taki kod, wierzc w jego poprawno:
typedef TArray<TArray<double>> MATRIX; // oj!
Ta wiara zostaje jednak doc szybko podkopana. Coraz czciej wprawdzie zdarza si, e
kompilator poprawnie rozpoznaje znaki >> jako zamykajce nawiasy ostre. Niemniej,
nadal moe to jeszcze powodowa bd lub co najmniej ostrzeenie.
Poprawna wersja kodu, dziaajca w kadej sytuacji, to oczywicie:
typedef TArray<TArray<double> > MATRIX; // OK
Dodatkowa spacja wyglda tu rzecz jasna bardzo nieadnie, ale pki co jest konieczna.
Wcale niewykluczone jednak, e za jaki czas take pierwsza wersja instrukcji typedef
bdzie musiaa by uznana za poprawn.
Nieoczywisty przykad
Mona susznie sdzi, e w powyszym przykadzie rozpoznanie sekwencji >> jako pary
nawiasw zamykajcych (a nie operatora przesunicia w prawo) nie jest zadaniem ponad
siy kompilatora. Pamitajmy aczkolwiek, e nie zawsze jest to takie oczywiste.
Spjrzmy choby na tak deklaracj:
TStaticArray<int, 16>>2> aInty; // chyba prosimy si o kopoty...
Dla czytajcego (i piszcego) kod czowieka jest cakiem wyrane widoczne, e drugim
parametrem szablonu TStaticArray jest tu 16>>2 (czyli 64). Kompilator uczulony na
problem nawiasw ostrych zinterpretuje aczkolwiek ponisz linijk jako:
TStaticArray<int, 16> >2> aInty; // ojej!
W sumie wic nie bardzo wiadomo, co jest lepsze. Waciwie jednak wyraenia podobne
do powyszego s raczej rzadkie i prawd mwic nie powinny by w ogle stosowane.
Gdyby zachodzia taka konieczno, najlepiej posuy si pomocniczymi nawiasami
okrgymi:
TStaticArray<int, (16>>2)> aInty; // OK
Wniosek z tego jest jeden: kiedy chodzi o nawiasy ostre i szablony, lepiej by
wyrozumiaym dla kompilatora i w odpowiednich miejscach pomc mu w zrozumieniu, o
co nam tak naprawd chodzi.
Ciekawostka: dlaczego tak si dzieje
By moe zastanawiasz si, dlaczego kompilator ma w ogle problemy z poprawnym
rozpoznawianiem uycia nawiasw ostrych. Przecie nic podobnego nie dotyczy ani
nawiasw okrgych (wyraenia, wywoania funkcji, itd.), ani nawiasw kwadratowych
(indeksowanie tablicy), ani nawet nawiasw klamrowych (bloki kodu). Skd wic
problemy wynikaj problemy objawiajce si w szablonach?
Szablony 519
Przyczyn jest po czci sposb, w jaki kompilatory C++ dokonuj analizy kodu.
Dokadne omwienie tego procesu jest skomplikowane i niepotrzebne, wic je sobie
darujemy. Interesujc nas czynnoci jest aczkolwiek jeden z pierwszych etapw
przetwarzania - tak zwana tokenizacja (ang. tokenization).
Tokenizacja polega na tym, i kompilator, analizujc kod znak po znaku, wyrnia w nim
elementy leksykalne jzyka - tokeny. Do tokenw nale gwnie identyfikatory (nazwy
zmiennych, funkcji, typw, itp.) oraz operatory. Kompilator wpierw dokonuje ich analizy
(parsowania) i tworzy list takich tokenw.
Sk polega na tym, e C++ jest jzykiem kontekstowym (ang. context-sensitive
language). Oznacza to, e identyczne sekwencje znakw mog w nim znaczy zupenie
co innego w zalenoci od kontekstu. Przykadowo, fraza a*b moe by zarwno
mnoeniem zmiennej a przez zmienn b, jak te deklaracj wskanika na typ a o nazwie
b. Wszystko zaley od znaczenia nazw a i b.
W przypadku operatorw mamy natomiast jeszcze jedn zasad, zwan zasad
maksymalnego dopasowania (ang. maximum match rule). Mwi ona, e naley
zawsze prbowa uj jak najwicej znakw w jeden token.
Te dwie cechy C++ (kontekstowo i maksymalne dopasowanie) daj w efekcie
zaprezentowane wczeniej problemy z nawiasami ostrymi. Problem jest bowiem w tym, i
zalenie od kontekstu i ssiedztwa znaki < i > mog by interpretowane jako:
operatory wikszoci i mniejszoci
operatory przesunicia bitowego
nawiasy ostre
Nie ma to wikszego znaczenia, jeli nie wystpuj one w bliskim ssiedztwie. W
przeciwnym razie zaczynaj si powane kopoty - jak choby tutaj:
TSomething<32>>4 > FOO> CosTam; // no i?...
Najlogiczniej wic byoby unika takich ryzykownych konstrukcji lub opatrywa je
dodatkowymi znakami (spacjami, nawiasami okrgymi), ktre umoliwi kompilatorowi
jednoznaczn interpretacj.
Nazwy zalene
Problem nawiasw ostrych jest w zasadzie kwesti wycznie skadniow, spowodowan
faktem wyboru takiego a nie innego rodzaju nawiasw do wsppracy z szablonami.
Jednak jeli nawet sprawy te zostayby kiedy rozwizane (co jest mao prawdopodobne,
zwaywszy, e pitego rodzaju nawiasw jeszcze nie wymylono :D), to i tak kod
szablonw w pewnych sytuacjach bdzie kopotliwy dla kompilatora.
O co dokadnie chodzi? Ot trzeba wiedzie, e szablony s tak naprawd kompilowane
dwukrotnie (albo raczej w dwch etapach):
najpierw s one analizowane pod ktem ewentualnych bdw skadniowych i
jzykowych w swej czystej (nieskonkretyzowanej) postaci. Na tym etapie
kompilator nie ma informacji np. o typach danych, do ktrych odnosz
symboliczne oznaczenia parametrw szablonw (T, TYP, itd.)
pniej produkty konkretyzacji s sprawdzane pod ktem swej poprawnoci w
cakiem normalny ju sposb, zbliony do analizy zwykego kodu C++
Nie byoby w tym nic niepokojcego gdyby nie fakt, e w pewnych sytuacjach kompilator
moe nie by wystarczajco kompetentny, by wykona faz pierwsz. Moe si bowiem
okaza, e do jej przeprowadzania wymagane s informacje, ktre mona uzyska
dopiero po konketyzacji, czyli w fazie drugiej.
Zaawansowane C++ 520
Pewnie w tej chwili nie bardzo moesz sobie wyobrazi, o jakie informacje moe tutaj
chodzi. Powiem wic, e sprawa dotyczy gwnie waciwej interpretacji tzw. nazw
zalenych.
Nazwa zalena (ang. dependent name) to kada nazwa uyta wewntrz szablonu,
powizana w jaki sposb z jego parametrami.
Fakt, e nazwy takie s powizane z parametrami szablonu, sprawia, e ich znaczenie
moe by rne w zalenoci od parametrw tego szablonu. Te wszystkie engimatyczne
stwierdzenia stan si bardziej jasne, gdy przyjrzymy si konkretnym przykadom
problemw i sposobom na ich rozwizanie.
Sowo kluczowe typename
Czas wic na kawaek szablonu :) Popatrzmy na tak problematyczn funkcj, ktra ma
za zadanie wybra najwikszy spord elementw tablicy:
template <class TAB> TAB::ELEMENT Najwiekszy(const TAB& aTablica)
{
// zmienna na przechowanie wyniku
TAB::ELEMENT Wynik = aTablica[0];
// ptla szukajca
for (unsigned i = 1; i < aTablica.Rozmiar(); ++i)
if (aTablica[i] > Wynik)
Wynik = aTablica[i];
// zwrcenie wyniku
return Wynik;
}
Mona si zdziwi, czemu parametrem szablonu jest tu typ tablicy (czyli np.
TArray<int>), a nie typ jej elementw (int). Dziki temu funkcja jest jednak bardziej
uniwersalna i niekoniecznie musiy wsppracowa wycznie z tablicami TArray.
Przeciwnie, moe dziaa dla kadej klasy tablic (a wic np. dla TOptimizedArray i
TSuperFastArray z paragrafiu o metaszablonach), ktra ma:
operator indeksowania
metod Rozmiar()
alias ELEMENT na typ elementw tablicy
Niestety, ten ostatni punkt jest wanie problemem. cilej mwic, to fraza
TAB::ELEMENT stanowi kopot - ELEMENT jest tu bowiem nazw zalen. My jestemy tu
wicie przekonani, e reprezentuje ona typ (int dla TArray<int>, itd.), jednak
kompilator nie moe bra takich informacji znikd. On faktycznie musi to wiedzie, aby
mg uzna m.in. deklaracj:
TAB::ELEMENT Wynik;
za poprawn. A skd ma si tego dowiedzie? Nie ma ku temu adnej moliwoci na
etapie analizy samego szablonu. Dopiero konkretyzacja, gdy TAB jest zastpowane
prawdziwym typem danych, daje mu tak moliwo. Tyle e aby w ogle mogo doj do
konkretyzacji, szablon musi najpierw przej test poprawnoci. Mwic wprost: aby
skontrolowa bezbdno szablonu kompilator musi najpierw skontrolowa bezbdno
szablonu :D Dochodzimy zatem do bdnego koa.
A wyjcie z niego jest jedno. Musimy w jaki sposb da do zrozumienia kompilatorowi,
e TAB::ELEMENT jest typem, a nie statycznym polem - bo taka jest wanie druga
Szablony 521
moliwa interpretacja tej konstrukcji. Czynimy to poprzedzajc problematyczn fraz
swkiem typename:
typename TAB::ELEMENT Wynik;
Deklaracja nieco nam si rozwleka, ale w przy korzystaniu z szablonw jest to ju chyba
regu :) W kadym razie teraz nie bdzie ju problemw ze zmienn Wynik; do penej
satysfakcji naley jeszcze podobny zabieg zastosowa wobec typu zwracanego przez
funkcj:
template <class TAB>
typename TAB::ELEMENT Najwiekszy(const TAB& aTablica)
Podobnie naley postpi z kadym wystpieniem TAB::ELEMENT w tym szablonie.
Powiem nawet wicej, formuujc ogln zasad:
Naley poprzedza sowem typename kad nazw zalen, ktra ma by
interpretowana jako typ danych.
Stosujc si do niej, nie bdziemy wprawia w kompilatora w zakopotanie i oszczdzimy
sobie dziwnie wygldajcych komunikatw o bdach.
Ciekawostka: konstrukcje ::template, .template i ->template
Podobny, cho znacznie raczej ujawniajcy si problem dotyczy szablonw
zagniedonych. Oto nieszczeglnie sensowny przykad takiej sytuacji:
template <typename T> class TFoo
{
public:
// zagniedony szablon klasy
template <typename U> class TBar
{
public:
// zagniedony szablon statycznej metody
template <typename V> static void Baz();
}
};
Pytanie brzmi: jak wywoa metod Baz()? No c, wyglda to moe tak:
template <typename T> void Funkcja()
{
// wywoanie jako statycznej metody bez obiektu
TFoo<T>::template TBar<T>::Baz();
// utworzenie lokalnego obiektu i wywoanie metody
typename TFoo<T>::template TBar<T> Bar;
Bar.template Baz<T>();
// utworzenie dynamicznego obiektu i wywoanie metody
typename TFoo<T>::template TBar<T>* pBar;
pBar = new typename TFoo<T>::template TBar<T>;
pBar->template Baz<T>();
delete pBar;
}
Zaawansowane C++ 522
Wiem, e wyglda to jak skryowanie trolla z goblinem, ale mwimy teraz o naprawd
specyficznym szczegliku, ktrego uycie jest bardzo rzadkie. Powyszy kod wyglaby
pewnie przejrzyciej, gdyby usun z niego wyrazy typename i template:
// UWAGA: ten kod NIE JEST poprawny!
// wywoanie jako statycznej metody bez obiektu
TFoo<T>::TBar<T>::Baz();
// utworzenie lokalnego obiektu i wywoanie metody
TFoo<T>::TBar<T> Bar;
Bar.Baz<T>();
// utworzenie dynamicznego obiektu i wywoanie metody
TFoo<T>::TBar<T>* pBar;
pBar = new TFoo<T>::TBar<T>;
pBar->Baz<T>();
delete pBar;
Tym samym jednak pozbawiamy kompilator informacji potrzebnych do skompilowania
szablonu. Rol typename znamy, wic zajmijmy si dodatkowymi uyciami template.
Ot tutaj template (a waciwie ::template, .template i ->template) suy do
poinformowania, e nastpujca dalej nazwa zalena jest szablonem. Patrzc na
definicj TFoo wiemy to oczywicie, jednak kompilator nie dowie si tego a do chwili
konkretyzacji. Dla niego nazwy TBar i Baz mog by rwnie dobrze skadowymi
statycznymi, za nastpujce dalej znaki < i > - operatorami relacji. Musimy wic
wyprowadzi go bdu.
Stosuj kontrukcje ::template, .template i ->template zamiast samych operatorw
::, . i -> w tych miejscach, gdzie podana dalej nazwa zalena jest szablonem.
Stosowalno tych konstrukcji jest wic ograniczona i zawa si do przypadkw
zagniedonych szablonw. W codziennej i nawet troch bardziej niecodziennej praktyce
programistycznej mona si bez nich obej, aczkolwiek warto o nich wiedzie, by mc je
zastosowa w tych nielicznych sytuacjach ujawniajcej si niewiedzy kompilatora.
Organizacja kodu szablonw
Wykorzystanie szablonw moe nastrcza problemw natury logistycznej. Nie chodzi o
sam czynno ich implementacji czy pniejszego wykorzystania, ale o, zdawaoby si:
prozaiczn, spraw nastpujc: jak rozmieci kod szablonw w plikach z kodem
programu?
Sprawa nie jest wcale taka prosta, bo kod korzystajcy z szablonw rni si znacznie
pod tym wzgldem od zwykego, nieszablonowego kodu. W sumie mona powiedzie,
e szablony s czym midzy normalnymi instrukcjami jzyka, a dyrektywami
preprocesora.
Ten fakt wpywa istotnie na sposb ich organizacji w programie. Obecnie znanych jest
kilka moliwych drg prowadzcych do celu; nazywamy je modelami. W tym paragrafie
popatrzymy sobie zatem na wszystkie trzy modele porzdkowania kodu szablonw.
Model wczania
Najwczeniejszym i do dzi najpopularniejszym sposobem zarzdzania szablonami jest
model wczania (ang. inclusion model). Jest on jednoczenie cakiem prosty w
stosowaniu i czsto wystarczajcy. Przyjrzyjmy mu si.
Szablony 523
Zwyky kod
Wpierw jednak przypomnimy sobie, jak naley radzi sobie z kodem C++ bez szablonw.
Ot, jak wiemy, wyrniamy w nim pliki nagwkowe oraz moduy kodu. I tak:
pliki nagwkowe s opatrzone rozszerzeniami .h, .hh, .hpp, lub .hxx i zawieraj
deklaracje wspuytkowanych czci kodu. Nale do nich:
prototypy funkcji
deklaracje zapowiadajce zmiennych globalnych (opatrzone sowem
extern)
definicje wasnych typw danych i aliasw, wprowadzane sowami typedef,
enum, struct, union i class
implementacje funkcji inline
moduy kodu s z kolei wyrzniane rozszerzeniami .c, .cc, .cpp lub .cxx i
przechowuj definicje (tudzie implementacje) zadeklarowanych w nagwkach
elementw programu. S to wic:
instrukcje funkcji globalnych oraz metod klas
deklaracje zmiennych globalnych (bez extern) i statycznych pl klas
Ten system, spity dyrektywami #include, dziaa wymienicie, oddzielajc to, co jest
wane do stosowania kodu od technicznych szczegw jego implementacji. Co si jednak
dzieje, gdy na scen wkraczaj szablony?
Prbujemy zastosowa szablony
Sprbujmy wic podobn metod zastosowa wobec szablonu funkcji max(). Najpierw
umiemy jej prototyp (deklaracj) w pliku nagwkowym:
// max.hpp
// prototyp szablonu max()
template <typename T> T max(T, T);
Nastpnie tre funkcji podamy w module kodu max.cpp:
// max.cpp
#include "max.hpp"
// implementacja szablonu max()
template <typename T> T max(T a, T b)
{
return (a > b ? a : b);
}
Wreszcie, wykorzystamy nasz funkcj w programie, wypisujc na przykad na ekranie
wiksz z podanych liczb:
// TemplatesTryout - prba zastosowania szablonu funkcji
// main.cpp
#include <iostream>
#include <conio.h>
#include "max.hpp"
int main()
{
std::cout << "Podaj dwie liczby:" << std::endl;
double fLiczba1, fLiczba2;
Zaawansowane C++ 524
std::cin >> fLiczba1;
std::cin >> fLiczba2;
std::cout << "Wieksza jest liczba " << max(fLiczba1, fLiczba2);
getch();
}
Pieczoowicie wykonujc te proste w gruncie rzeczy czynnoci, mamy prawo czu si
zaskoczeni efektami. Prba wygenerowania gotowego programu skoczy si bowiem
komunikatem linkera zblionym do poniszego:
error LNK2019: unresolved external symbol "double __cdecl max(double,double)" (?max@@YANNN@Z)
referenced in function _main
Wynika z niego klarownie, e funkcja max() w wersji skonkretyzowanej dla double nie
istnieje! Jak to wyjani?
Wytumaczenie jest w miar proste. Zwr uwag, e doczenie pliku max.hpp wcza
do main.cpp jedynie deklaracj szablonu, a nie jego definicj. Nie majc definicji
kompilator nie moe natomiast skonkretyzowa szablonu dla parametru double. Wobec
tego czyni on zaoenie, e funkcja max<double>() zostaa wygenerowana gdzie indziej.
Nie ma w tym nic zdronego - ten sam mechanizm dziaa przecie dla zwykych funkcji,
ktre s deklarowane (prototypowane) w pliku nagwkowym, a implementowane w
innym module. Niestety, w tym przypadku jest to zaoenie bdne: konkretyzacja nie
zostanie bowiem przeprowadzona z powodu wspomnianego braku informacji (definicji
szablonu).
Ostatecznie wic powstaje zewntrzne dowizanie do specjalizacji szablonu max() dla
parametru double - specjalizacji, ktra nie istnieje! Ten fakt nie umknie ju uwadze
linkera, czego skutkiem jest zaprezentowany wyej bd i poraka konsolidacji.
Rozwizanie - istota modelu wczania
Sytuacja patowa? Bynajmniej. Istnieje oczywicie rozwizanie tego problemu: trzeba po
prostu zapewni widoczno definicji szablonu max() (czyli zawartoci max.cpp) w
miejscu jego uycia (czyli main.cpp). Mona to uczyni poprzez:
doczenie zawartoci max.cpp do max.hpp (dodanie #include "max.cpp" na
kocu max.hpp)
doczenie max.cpp w module main.cpp zamiast doczania max.hpp
przeniesienie zawartoci moduu max.cpp (czyli definicj szablonu max()) do pliku
nagwkowego max.hpp
Wszystkie te sposoby s wariantami modelu wczania, o ktrym mwimy w tym
paragrafie. Zastosowanie ktregokolwiek spowoduje podany efekt, czyli poprawn
kompilacj kodu. W praktyce jednak najczciej stosuje si sposb trzeci, czyli
umieszczanie caego kodu szablonw w pliku nagwkowym.
Mimo takiego postpowania funkcje szablonowe nie bd rozwijane w miejscu
wywoania. Aby szablon funkcji by funkcj inline, naley jawnie poprzedzi j
przydomkiem inline po klauzuli template <...>.
Model wczania dziaa cakiem dobrze zarwno dla maych, jak i nieco wikszych
rednich projektw. Jest z nim jednak zwizany pewien mankament: w oczywisty sposb
powoduje on rozrost plikw nagwkowych. Sprawia to, e koszt ich doczania staje si
coraz wikszy, co w konsekwencji wydua czas kompilacji projektw. Staje si to
aczkolwiek zauwaalne i znaczce dopiero w naprawd duych programach (rzdu
kilkunastu-kilkudziesiciu tysicy linii).
Szablony 525
W sumie mona wic powiedzie, e model wczania jest zadowolajcym sposobem
zarzdzania kodem szablonw. Nie jest to jednak wystarczajcy argument za tym, aby
nie przyjrze si take innym modelom :)
Konkretyzacja jawna
W bdnym przykadzie programu z szablonem max() problem polega na tym, e
kompilator nie mia okazji do waciwego skonkretyzowania szablonu. Model wczania
umoliwia mu to w sposb automatyczny.
Istnieje aczkolwiek inna metoda na rozwizanie tego problemu. Moemy mianowicie
zastosowa model konkretyzacji jawnej (ang. explicit instantiation) i przej kontrol
nad procesem rozwijania szablonw. Zobaczmy zatem, jak mona to zrobi.
Instrukcje jawnej konkretyzacji
Wyjaniem, e powodem komunikatu linkera i nieudanej konsolidacji przykadu z
poprzedniego akapitu jest nieobecno funkcji max<double>() w adnym ze
skompilowanych moduw. Moemy to zmieni, sami wprowadzajc rzeczon funkcj -
czyli jawnie j skonkretyzowa. Czynimy w nastpujcy sposb:
// max_inst.cpp
#include "max.cpp"
// jawna konkretyzacjia szablonu max() dla parametru double
template double max<double>(double, double);
Mamy tutaj dyrektyw konkretyzacji jawnej (ang. explicit instantiation directive). Jak
wida, skada si ona z samego sowa template (bez nawiasw ostrych) oraz penej
deklaracji specjalizacji szablonu (czyli max<double>()). Tutaj akurat mamy funkcj, ale
podobnie konkretyzacja jawna wyglda klas. W kadym przypadku konieczna jest
definicja konkretyzowanego szablonu - std doczenie do naszego nowego moduu
pliku max.cpp.
Nalezy zwrci uwag, aby kada specjalizacja szablonu bya wprowadzana jawnie tylko
jeden raz. W przeciwnym razie zwrci na to uwag linker.
Wady i zalety konkretyzacji jawnej
Zastosowanie takiego wybiegu spowoduje teraz poprawn kompilacj i linkowanie
programu. Moemy si wic przekona, e konkretyzacja jawna faktycznie dziaa.
Nie ma jednak ry bez kolcw. Ten sposb zarzdzania specjalizacjami szablonu ma
oczywist wad - jedn, ale za to bardzo dotkliw. Wymaga on od programisty ledzenia
kodu, ktry wykorzystuje szablony, celem rozpoznawania wymaganych specjalizacji oraz
ich jawnego deklarowania. Zwykle robi si to w osobnym module (u nas max_inst.cpp),
aby nie zamieca waciwego kodu programu.
Nie da si ukry, e niweluje to jedn z bezdyskusyjnych zalet szablonw, czyli moliwo
zrzucenia na barki kompilatora kwestii wygenerowania waciwego ich kodu. Jest to
szczeglnie niezadowalajce w przypadku szablonw funkcji, gdzie przy kadym ich
wywoaniu musimy zastanowi si, jaka wersja szablonu zostanie w tym konkretnym
wypadku uyta. Faktycznie wic trudno nawet czerpa korzyci z automatycznej dedukcji
parametrw szablonu na podstawie parametrw funkcji - a to jest przecie jedno z
gwnych dobrodziejstw szablonw.
Konkretyzacja jawna ma aczkolwiek take kilka zalet, do ktrych nale:
moliwo sprawowania kontroli nad procesem rozwijania szablonw
zapobieganie nadmiernego rozdciu plikw nagwkowych, a wic potencjalne
skrcenie czasu kompilacji
Zaawansowane C++ 526
umoliwie dokadnego okrelenia miejsca (moduu kodu), w ktrym egzemplarz
szablonu (specjalizacja) zostanie utworzony
W wikszoci przypadkw te argumenty nie s jednak wystarczajce, aby mogy
przeway na rzecz wykorzystania modelu konkretyzacji jawnej w praktyce. Podobnie
bowiem jak w przypadku modelu wczania, rozrost programu powoduje take
wyduenie czasu przeznaczonego na konkretyzacj. Rnica tkwi jednake w tym, e w
tym pierwszym modelu ca prac zajmuje si kompilator, ktry i tak nie ma nic
ciekawszego do roboty, natomiast konkretyzacja jawna zrzuca ten obowizek na barki
wiecznie zapracowanego programisty.
W sumie wic ten model organizacji szablonw trudno uzna za praktyczny i wygodny.
By moe sprawdziby si niele w maych programach, ale tam mona sobie przecie
tym bardziej pozwoli na znacznie wygodniejszy model wczania.
Model separacji
Lekarstwem na bolczki modelu wczania ma by mechanizm eksportowania
szablonw. Technika ta, nazywana rwnie modelem separacji, jest czci samego
jzyka C++ i teoretycznie jest to wanie ten sposb zarzdzania kodem szablonw,
ktry ma by preferowany. Przynajmniej tako rzecze Standard C++.
Tym niemniej ju od razu powiadomi, e w miar poprawna obsuga tego modelu jest
dostpna dopiero w Visual Studio .NET 2003.
Wypadaoby zatem pozna bliej to natywne rozwizanie samego jzyka.
Szablony eksportowane
Idea tego modelu jest generalnie bardzo prosta:
zachowany zostaje naturalny porzdek oddzielania deklaracji/definicji od
implementacji. W pliku nagwkowym umieszczamy wic wycznie deklaracje
(prototypy) szablonw funkcji oraz definicje szablonw klas. Postepujemy zatem
tak, jak prbowalimy czyni na samym pocztku - dopki linker nie sprowadzi
nas na ziemi
zmiana polega jedynie na tym, e deklaracj szablonu w pliku nagwkowym
opatrujemy sowem kluczowym export
Stosujc te dwie wskazwki do naszego bdnego przykadu TemplatesTryout,
naleaoby jedynie zmodyfikowa plik max.hpp. Zmiana ta jest zreszt niemal
kosmetyczna:
// max.hpp
// prototyp szablonu max() jako szablon eksportowany
export template <typename T> T max(T, T);
Jak si wydaje, dodanie sowa export przed deklaracj szablonu zaatwia spraw.
W rzeczywistoci sowo to powinno si znale przed kadym uyciem klauzuli template
<...>. export ma jednak t przyjemn waciwo, e po jednokrotnym jego
zastosowaniu w obrbie danego pliku z kodem wszystkie dalsze szablony otrzymuj
ten przydomek niejawnie. A dziki temu, e w pliku max.cpp znajduje si odpowiednia
dyrektywa #include:
// max.cpp
#include "max.hpp"
Szablony 527
// (dalej implementacja szablonu max())
rwnie kod szablonu funkcji max() dostaje modyfikator export w prezencie od pliku
nagwkowego max.hpp. Jeli wic zdecydujemy si pisa kod szablonw w identyczny
sposb, jak zwyky kod C++, to nasza troska o waciw kompilacj szablonw powinna
ogranicza si do dodawania sowa kluczowego export przed deklaracjami template
<...> w plikach nagwkowych.
Przynajmniej teoretycznie tak wanie powinno by
Nie ma ry bez kolcw
Model separacji moe ci si teraz wydawa rodzajem biaej magii, likwidujcej wszystkie
mankamenty organizacji kodu szablonw. Trzeba sobie jednak zdawa spraw, e nie
jest on pozbawiony wad. Czas wic zdj z twarzy ten szczliwy umieszek i przyjrze
si rzeczywistoci.
A rzeczywisto skrzeczy. Przede wszystkim naley wiedzie, e mimo kilkuletniej ju
obecnoci w Standardzie C++ i w wiadomoci sporej czci programistw (przynajmniej
tych co bardziej zainteresowanych rozwojem jzyka), szablony eksportowane s w peni
obsugiwane przez nieliczne kompilatory. Dopiero ich najnowsze wersje (jak na przykad
Visual Studio .NET 2003) radz sobie ze sowem kluczowym export.
Ze wzgldu na tak nike dowiadczenia praktyczne trudno te przewidzie potencjalne
problemy, jakie mog (cho oczywicie nie musz) przydarzy si podczas korzystania z
modelu separacji. Te rzadkie kompilatory radzce sobie z tym modelem mog bowiem
dziaa wietnie przy maych czy nawet rednich projektach, ale nie jest wcale
powiedziane, czy przy wikszych programach nie ujawni si w nich jakie kopoty.
Wiadomo wszake, e najlepszym probierzem jakoci wszelkich produktw - take
moliwoci kompilatorw - jest ich intensywne wykorzystywanie przez rzesze
uytkownikw. W tym za przypadku nie jest to jeszcze powszechn praktyk
(przynajmniej nie tak bardzo, jak inne elementy C++), cho naley rzecz jasna
oczekiwa, e sytuacji bdzie si z czasem poprawia.
Druga sprawa zwizana jest z samym dziaaniem sowa kluczowego export. W
przyblieniu mona je scharakteryzowa jako ukrycie funkcjonalnoci nieeleganckiego
modelu wczenia - oczywicie wraz z pewnymi usprawnieniami. Oznacza to wic, e nie
dokonuj si tu adne cuda: pozorne zerwanie zwizku midzy definicj a konkretyzacj
szablonu musi i tak by odtworzone przez kompilator. To sprawia, e jakoby niezalene
od siebie moduy kodu staj si zwizane wanie ze wzgldu na obecno w nich
implementacji szablonw. W ostatecznoci koszt czasowy kompilacji programu wcale nie
musi by wiele mniejszy od tego, jaki jest dowiadczany w modelu wczania.
Wszystko to nie znaczy jednak, e nie naley spoglda na model separacji przychylnym
okiem. Czas dziaa bowiem na jego korzy. Gdy obsuga szablonw eksportowanych
stanie si powszechna, postpowa bdzie take jej usprawnienie pod wzgldem
niezawodnoci i efektywnoci. Wcale niewykluczone, e na tym polu zostawi za jaki czas
daleko w tyle model wczania.
A ju teraz model separacji oferuje nam zalet niespotykan w innych rozwizaniach
problemu szablonw: elegancj, podobn do tej znanej ze zwykego, nieszablonowego
kodu. Dalej bdzie zapewne ju tylko lepiej.
Wsppraca modelu wczania i separacji
Ucieszy moe take fakt, e stosunkowo atwo zorganizowa kod szablonw w taki
sposb, aby przeczanie midzy modelem wczania i separacji nie zajmowao wicej
ni kilka sekund (nie liczc rekompilacji). Dosy dobrze do tego celu nadaj si
dyrektywy preprocesora.
Zaawansowane C++ 528
Pomys jest prosty. Naley tak zmodyfikowa plik nagwkowy z deklaracj szablonu (u
nas max.hpp), by w razie potrzeby zawiera on rwnie jego definicj - czyli wcza j
z moduu kodu (max.cpp). Oto propozycja takiej modyfikacji:
// max.hpp
// zabezpieczenie przed wielokrotnym doczaniem - wane!
#pragma once
// w zalenoci od tego, czy zdefiniowano makro USE_EXPORT,
// wprowadzamy do programu sowo kluczowe export
#ifdef USE_EXPORT
#define EXPORT export
#else
#define EXPORT
#endif
// deklaracja szablonu
EXPORT template <typename T> T max(T, T);
// jeeli nie uywamy modelu separacji, to potrzebujemy take
// definicji szablonu. Wczamy j wic
#ifndef USE_EXPORT
#include "max.cpp"
#endif
Decyzja co do uywanego modelu ogranicza si tu bdzie do zdefiniowania lub
niezdefiniowania makra USE_EXPORT przed doczeniem pliku max.hpp:
// uywanie modelu separacji; bez #define bdzie to model wczania
#define USE_EXPORT
#include "max.hpp"
Trzeba jeszcze pamita, aby w tym pliku nagwkowym przynajmniej pierwsz
deklaracj szablonu (a najlepiej wszystkie) opatrzy nazw makra EXPORT. W zalenoci
od wybranego modelu bdzie ono bowiem rozwinite do sowa export lub do pustego
cigu, co w wyniku da nam zastosowanie wybranego modelu.
Opisana sztuczka opiera si, w przypadku uycia modelu wczania, o sprzenie
zwrotne dyrektyw #include: max.hpp docza bowiem max.cpp, za max.cpp prbuje
doczy max.hpp. Trzeba rzecz jasna zadba o to, by ta druga prba nie zakoczya si
powodzeniem, stosujc jedno z zabezpiecze przeciw wielokrotnemu doczaniu. Tutaj
uyem #pragma once, cho metoda z unikalnym makrem oraz #ifndef/#endif rwnie
zdaaby egzamin.
***
I tak oto zakoczylimy drugi podrozdzia powicony opisowi szablonw w C++. W
zasadzie moesz uzna ten moment za koniec teorii tego skomplikowanego zagadnienia.
Chocia wic zajmowalimy si ju sprawami bardziej praktycznymi (jak choby modelem
organizacji kodu), to dopiero w nastpnym podrozdziale poznasz prawdziwe zastosowania
szablonw. Zacznie si wic robi bardzo ciekawie, jako e dopiero w konkretnych
metodach na wykorzystanie szablonw wida prawdziw potg tego skadnika C++.
Pora zatem j ujarzmi!
Szablony 529
Zastosowania szablonw
Jeszcze w pocztkach tego rozdziau powiedziaem, do czego su szablony w jzyku
C++. Przypominam: stosujemy je gwnie tam, gdzie chcemy uniezaleni kod programu
od konkretnego typu danych.
To oglnikowe stwierdzenie jest z pewnoci pomocne, ale mao konkretne. Na pewno
bdziesz bardziej zadowolony, jeeli ujrzysz jakie precyzyjniej okrelone zastosowania
dla szablonw. I to jest wanie treci tego podrozdziau. Pomwimy sobie wic o
niektrych sytuacjach, gdy skorzystanie z szablonw uatwia lub wrcz umoliwia
wykonanie wanych programistycznych zada.
Zastpienie makrodefinicji
Gdyby to bya bajka, to zaczoby si tak: dawno, dawno temu w krlestwie Elastycznych
Programw niepodzielnie rzdzia okrutna kasta Makrodefinicji. Do czsto utrudniaa
ona ycie mieszkacom, powodujc wiksze lub mniejsze yciowe uciliwoci. Na
szczcie pewnego dnia na pomoc przybyli dzielni rycerze Szablonw, ktrzy obalili
tyranw i zapewnili krlestwu szczliwe ycie pod rzdami nowych, askawych wadcw.
I wszyscy yli dugo i szczliwie.
To tyle, jeli chodzi o otoczk baniow, bo teraz naleaoby wrci do rzeczywistego
zagadnienia. Jaki czas temu mielimy okazj pozna dyrektywy preprocesora, zwracajc
przy tym szczegln uwag na makra. Makra imitujce funkcje byy kiedy jedynym
sposobem na tworzenie kodu niezwizanego z adnym typem danych. Teraz za mamy
ju szablony. Czy s one lepsze?
Szablon funkcji i makro
Aby si o tym przekona, porwnajmy funkcj max() - napisan raz w postaci szablonu i
drugi raz w postaci makra:
// szablon funkcji max()
template <typename T> T max(T a, T b) { return (a > b ? a : b); }
// makro MAX()
#define MAX(a,b) ((a) > (b) ? (a) : (b))
Wida par podobiestw, ale i mnstwo rnic. Przede wszystkim interesuje nas to, w
jaki sposb makra i szablony osigaj niezaleno od typu danych - parametrw. W
sumie wiemy to dobrze:
w szablonach wystpuj parametry bdce typami (jak u nas T),
nieodpowiadajce jednak adnemu konkretnemu typowi danych. Poprzez
konkretyzacj tworzone s potem specjalizowane egzemplarze funkcji, dziaajce
dla cile okrelonych ju rodzajw zmiennych
makra w ogle nie posuguj si pojciem typ danych. Ich istota polega na
zwykej zamianie jednego tekstu (wywoania makra) w inny tekst (rozwinicie
makra). Dopiero to rozwinicie jest przedmiotem zainteresowania kompilatora,
ktry wedle swoich regu - jak choby poprawnego uycia operatorw - uzna je za
poprawne bd nie
Mamy wic dwa rne podejcia i zapewne ju wiesz lub domylasz si, e nie s one
rwnowane ani nawet rwnie dobre. Naley wic odpowiedzie na proste pytanie - co
jest lepsze?
Zaawansowane C++ 530
Pojedynek na szczycie
W tym celu sprbujmy uy obu zaprezentowanych wyej konstrukcji, poddajc je
swoistym prbom:
// bdziemy potrzebowali kilku zmiennych
int nA = 42; float fB = 12.0f;
// i startujemy...
std::cout << max(34, 56) << " | " << MAX(34, 56) << std::endl; // 1
std::cout << max(nA, fB) << " | " << MAX(nA, fB) << std::endl; // 2
std::cout << max(nA++, fB) << " | " << MAX(nA++, fB) << std::endl; // 3
Czy obie konstrukcje przejd je z powodzeniem? C, odpowied jest niestety
przeczca. Tylko pierwsza linijka nie wymaga adnych uwag ani analizy. W tym
przypadku nie ma po prostu adnych wtpliwoci: obie wartoci do porwnania s
jednoznacznymi staymi tych samych typw. Wszystko wic pjdzie gadko.
Jednak dalej zaczynaj si ju kopoty
Starcie drugie: problem dopasowania tudzie wydajnoci
Popatrzmy wic, co si waciwie stanie w tym kodzie. Pomylmy mianowicie, w jaki
sposb poradzi sobie z zadaniem szablon funkcji, a w jaki - makrodefinicja.
Jak zadziaa szablon
Funkcjonowanie szablonw byo przedmiotem sporej czci aktualnego rozdziau, zatem
odpowied na pytanie powyej nie powinna ci nastrcza trudnoci. Szablon max()
zadziaa tak, jak si spodziewamy: jego uycie spowoduje konkretyzacj dla waciwego
parametru, co w wyniku da normaln funkcj, wykorzystywan przez program.
Wpierw jednak musi by znany parametr T szablonu - zostanie on oczywicie
wydedukowany z wywoania funkcji max(). Mamy w nim argumenty bdce zmiennymi:
pierwsza jest typu int, za druga typu float. Parametr szablonu T jest natomiast tylko
jeden - c wic? Naturalnie, kompilator wybierze tak, aby nie skrzywdzi adnego z
argumentw funkcji, decydujc si na typ float. Pomieci on bowiem zarwno liczb
cakowit, jak i rzeczywist. Szablon max() zostanie wic skonkretyzowany do postaci:
float max<float>(float a, float b) { return (a > b ? a : b); }
I wszystko byoby w porzdku, gdyby nie jeden drobny niuans, w zasadzie
niedostrzegalny na pierwszy rzut oka. Jak to zwykle bywa w niejasnych sytuacjach,
chodzi o wydajno. Zwrmy uwag, e parametry funkcji max() s tu przekazywane
poprzez warto. Potencjalnie wic moe to prowadzi do dwch niepotrzebnych
kopiowa, wykonywanych podczas wywoywania funkcji w skompilowanym programie.
Oczywicie, ma to znaczenie tylko dla duych obiektw, lecz kto powiedzia, e nie
moglibymy chcie uy tej funkcji na przykad do 1000-elementowej tablicy?
Powiesz pewnie, e jest to na to rada. Wystarczy skorzysta z wynalazku C++ znanego
pod nazw referencji. Przypomnijmy, e referencje, czyli ukryte wskaniki, nie
powoduj przekazania do funkcji samego obiektu, lecz tylko jego adresu. Ich zalet jest
za to, e nie zmuszaj do korzystania z kopotliwej w gruncie rzeczy skadni
wskanikw.
Pamitajc o tym, ochoczo przerabiamy nasz szablon na wersj korzystajc z referencji:
template <typename T> T max(const T& a, const T& b)
{
return (a > b ? a : b);
}
Szablony 531
W ten sposb niechccy pozbawilimy kompilator wanej moliwoci: uywania
niejawnych konwersji. W momencie, gdy chcemy przekaza do funkcji nie obiekt, a
referencj do niego, kompilator staje si po prostu lepy na ten mechanizm jzyka.
atwo to zreszt wyjani: istot referencji jest odwoywanie si do istniejcego obiektu
bez kopiowania, za istniejcy obiekt ma swj typ, ktrego zmieni nie mona.
Wic co zrobi? Najlepiej po prostu pogodzi si z tym strasznym marnotrawstem,
ktre i tak nie jest szczeglnie wielkie, a przez dobry kompilator moe by nawet z
niezym skutkiem minimalizowane.
Naturalnie, mona prbowac kombinowa dalej - chociaby doda drugi parametr
szablonu. Tyle e wtedy pozostanie nierozstrzygalny wybr, ktry z nich uczyni typem
wartoci zwracanej. Naturalnie, mona ten typ doda jako kolejny, trzeci ju parametr
szablonu i kaza go podawa wywoujcemu. Wreszcie, mona nawet uy jednego z
kilku do pokrtnych (koncepcyjnie i skadniowo) sposobw na obejcie problemu - ale
chyba nie zmartwisz si tym, e ci ich tutaj oszczdz. Nadmierna komplikacja jest tu
bowiem wysoce niewskazana; zaangaowane rodki bd zwyczajnie niewspmierne do
zyskw.
Jak zadziaa makro
Przekonajmy si wic, co ma do powiedzenia makrodefinicja. Tutaj caa sprawa jest rzecz
jasna znacznie atwiejsza: preprocesor rozwinie nam po prostu kod MAX(nA, fB) do
postaci nastpujcego wyraenia:
((nA) ? (nB) ? (nA) : (nB))
Nie ma tutaj absolutnie rzadnej rnicy z sytuacj, w ktrej to wyraenie zostaoby
wpisane bezporednio do kodu. adna funkcja nie jest generowana, adne konwersje
argumentw nie s wykonywane, po prostu nie ma adnego przeskoku z miejsca
wywoania makra w inne miejsce programu. Kompilator jest wrcz utrzymywany w
bogiej niewiadomoci, gdy dostaje wyklarowany ju kod bez makr. Wszystkim zajmuje
si preprocesor i to on sprawia, e makro dziaa.
Wynik
Ostatecznie moemy uzna remis obu rozwiza, aczkolwiek z lekkim wskazaniem na
makrodefinicje. Z wyjtkiem fanatykw wydajnoci nie ma jednak bodaj nikogo, kto
uwaaby nieefektywne dziaanie szablonw za wielki bd. A tym, ktrzy rzeczywicie
tak uwaaj, pozostaje chyba tylko przerzucenie si na jzyk asemblera :)
Starcie trzecie: problem rozwinicia albo poprawnoci
Prba trzecia jest w takim razie decydujca. Ponownie rozoymy na czynniki pierwsze
sposb dziaania szablonu i makra.
Jak zadziaa szablon
Dziaanie szablonu bdzie tu udzco podobne do poprzedniej prby. Znowu bowiem
argumenty funkcji max() musza by dopasowane do typu oglniejszego - czyli do float.
Powstanie wic specjalizacja max<double>().
Funkcja ta bdzie potem wywoywana z argumentami nA++ i fB. Wobec tego zwrci ona
wiksz spord liczb: nA+1 i fB. Waciwie wic nie ma nad czym duej deliberowa;
nasz szablon zachowa si zupenie poprawnie, prawie jak zwyczajna funkcja. Naturalnie,
stosuj si tutaj wszystkie uwagi z poprzedniego akapitu - nie ma sensu ponownie ich
przytacza.
Ogem test uwaamy za zaliczony.
Zaawansowane C++ 532
Jak zadziaa makro
A teraz czas na analiz makrodefinicji i jej uycia w formie MAX(nA++, fB). Pamitajc,
jak dziaa preprocesor, susznie mona wywnioskowa, e zamieni on wywoanie makra
na takie oto wyraenie:
((nA++) > (fB) ? (nA++) : (fB))
Wszystko jest zatem w porzdku? Nie cakiem. Wrcz przeciwnie. Mamy problem.
Powany problem. A jego przyczyn jest obecno instrukcji nA++ dwukrotnie.
Fakt ten sprawi mianowicie, e zmienna nA zostanie dwa razy zwikszona o 1!
Ostatecznie warunek powyej zwrci bdny wynik - rnicy si od waciwego o ow
problematyczn jedynk.
Jeli pamitasz dokadnie rozdzia o preprocesorze, takie zachowanie nie powinno by dla
ciebie zaskoczeniem. Ju wtedy zaprezentowaem przykad tego problemu i ostrzegem
przed stosowaniem makrodefinicji w charakterze funkcji.
Wynik
C mona wicej powiedzie? Bdny rezultat uycia makra sprawia, e makrodefinicje
nie tylko przegrywaj, ale waciwie zostaj zdyskwalifikowane jako narzedzia tworzenia
kodu niezalenego od typu. Bezapelacyjnie wygrywaj szablony!
Konkluzje
Wniosek jest waciwie jeden:
Naley uywa szablonw funkcji zamiast makr, ktre maj udawa funkcje.
Makrodefinicje w rodzaju MAX(), MIN() czy innych tego rodzaju nie maj ju wic
waciwie racji bytu. Zastpiy je cakowicie szablony funkcji, oferujce nie tylko te same
rezultaty (przy zastosowaniu inline - rwnie wydajnociowe), ale te jedn konieczn
cech, ktrej makrom brak - poprawno.
Szablony s po prostu bardziej inteligentne, jako e odpowiada za nie przemylnie
skonstruowany kompilator, a nie jego uomny pomocnik - preprocesor. Jak si te miae
okazj przekona w tym rozdziale, moliwoci szablonw funkcji s nieporwnywalnie
wiksze od tych dawanych przez makrodefinicje.
Nie znaczy to oczywicie, e makra zostay cakowicie zastpione przez szablony. Nadal
bowiem znajduj one zastosowanie tam, gdzie chcemy dokonywa operacji na kodzie jak
na zwykym tekcie - a wic na przykad do wstawiania kilku czsto wystepujcych
instrukcji, ktrych nie moemy wyodrbni w postaci funkcji. Niemniej naley podkrela
(co robi po raz n-ty), e makra nie su do imitacji funkcji, gdy same funkcje (lub
ich szablony) doskonale radz sobie ze wszystkimi zadaniami, jakie chcielibymy im
powierzy. Naocznie to zreszt zobaczylimy.
Struktury danych
Szablony funkcji maj wic swoje wane zastosowanie. Waciwie jednak to szablony klas
s uyteczne w znacznie wikszym stopniu. Wykorzystujemy je bowiem w celu
implementacji w programach tzw. struktur danych.
Szablony 533
Jak gosi stare programistyczne rwnanie, obok algorytmw to struktury danych s
gwnymi skadnikami programw
127
. Jak wskazuje nazwa tego pojcia, su one do
przemylanej organizacji informacji przetwarzanych przez aplikacj. Zazwyczaj te
struktury danych cile wsppracuj z algorytmami programu.
Z najprostszymi strukturami danych zapoznae si ju cakiem dawno temu. Typowym
przykadem moe by zwyka, jednowymiarowa tablica; inny to np. struktura jzyka C++
(definiowana poprzez struct), zwana czasem rekordem. To jednak tylko wierzchoek
gry lodowej. Wrd wielu struktur danych wikszo jest o wiele bardziej
wyspecjalizowana i funkcjonalna.
C jednak ma to wsplnego z szablonami? Ot bardzo wiele. Dziki mechanizmowi
parametryzowanych typw (czyli szablonw klas) implementacja przernych struktur
danych w C++ jest prosta. Przynajmniej jest ona prosta w tym sensie, e nie nastrcza
kopotw zwizanych z nieokrelonymi typami danych. Szablony zaatwiaj za nas t
spraw, dziki czemu owe struktury mog by uniwersalne.
Prawdopodobnie wanie to zastosowanie byo jednym z gwnych powodw, dla ktrego
w ogle wprowadzono do jzyka C++ narzdzia szablonw. Nam pozostaje si tylko z
tego cieszy no, monaby jeszcze przyjrze si sprawie nieco bliej :) Zrbmy wic to.
W tej sekcji porozmawiamy sobie zatem o tym, jak szablony pomogaj w tworzeniu
struktur danych w programach. Naturalnie, temat ten jest niezwykle szeroki i dlatego nie
bdziemy w niego wnika dokadnie. Niemniej bdzie to dobra rozgrzewka przez
poznawaniem Biblioteki Standardowej, ktra szeroko uywa szablonw do implementacji
struktur danych.
Omwimy wic sobie dwie najprostsze kategorie takich struktur: krotki i kontenery
(pojemniki).
Krotki
Krotk (ang. tuple, nie myli ze stokrotk ;)) nazywamy poczenie kilku wartoci
rnych typw w jedn cao. C++, podobnie jak wiele innych jzykw
programowania umoliwia na zrealizowanie takiej koncepcji przy uyciu struktury,
zawierajcej dwa, trzy, cztery lub wiksz liczb pl dowolnych typw.
Tutaj jednak chcemy zobaczy w akcji szablony, zatem stworzymy nieco bardziej
elastyczne rozwizanie.
Przykad pary
Najprotsz krotk jest oczywicie pojedyncza warto :) Poniewa jednak w jej
przypadku do szczcia wystarcza normalna zmienna, zajmijmy si raczej zespoem
dwch wartoci. Zwiemy go par (ang. pair) lub duetem (ang. duo).
Definicja szablonu
Majc w pamici fakt, i chcemy otrzyma par dwch wartoci dwch rnych typw,
wyprodukujemy zapewne szablon podobny do poniszego:
template <typename T1, typename T2> struct TPair
{
T1 Pierwszy; // warto pierwszego pola
T2 Drugi; // warto drugiego pola
};
127
To rwnanie to Algorytmy + struktury danych = programy, bedce jednoczenie tytuem synnej ksiki
Niklausa Wirtha.
Zaawansowane C++ 534
Zastosowa takiej prostej struktury jest cae mnstwo. Przy jej uyciu moemy na
przykad w atwy sposb stosowa technik informowania o bdach przy pomocy
rezultatu funkcji. Oto przykad:
TPair<bool, T> Wynik = Funkcja(); // funkcja zwraca par wartoci
if (Wynik.Pierwszy)
{
// wykonanie funkcji powiodo si; jej waciwy rezultat to
// Wynik.Drugi
}
Wynik jako zesp dwch wartoci pozwala na oddzielenie waciwego rezultatu od
danych bdu. Jednoczenie nie zatracamy informacji o typie wartoci zwracanej przez
funkcj - tutaj ukrywa si on za T i jest widoczny w prototypie funkcji.
Pomocna funkcja
Do wygodnego uywania pary przydaby si sposb na jej atwie utworzenie. Na razie
bowiem Funkcja() musiaaby wykonywa np. taki kod:
TPair<bool, int> Wynik; // obiekt wyniku
Wynik.Pierwszy = true; // informacja o ewentualnym bdzie
Wynik.Drugi = 42; // zasadniczy rezultat
return Wynik; // zwracamy to wszystko
Sytuacj moemy poprawi, dodajc konstruktor(y):
template <typename T1, typename T2> struct TPair
{
T1 Pierwszy; // warto pierwszego pola
T2 Drugi; // warto drugiego pola
//-------------------------------------------------------------------
// konstruktory
TPair() : Pierwszy(), Drugi() { }
TPair(const T1& Wartosc1, const T2& Wartosc2)
: Pierwszy(Wartosc1), Drugi(Wartosc2) { }
};
W zasadzie to s one niezbdne - inaczej nie monaby tworzy par z obiektw, ktrych
klasy nie maj domylnych konstruktorw. Tak czy owak, skracamy ju zapis do
skromnego:
return TPair<bool, int>(true, 42);
Nadal jednak mona troch ponarzeka. Kompilator nie jest na przykad na tyle
inteligentny, aby wydedukowa parametry szablonu TPair z argumentw konstruktora.
To jednak mona atwo uzyska, jako e umiejtno takiej dedukcji jest nieodczn
cech szablonw funkcji. Moemy zatem stworzy sobie pomocn funkcj Para(),
tworzc duet:
template <typename T1, typename T2>
inline TPair<T1, T2> Para(const T1& Wartosc1, const T2& Wartosc2)
{
return TPair<T1, T2>(Wartosc1, Wartosc2);
}
To wreszcie pozwoli na stosowanie krtkiej i przemylanej formy tworzenia pary:
Szablony 535
return Para(true, 42);
Przydomek inline zabezpiecza natomiast przed niewybaczalnym uszczerbkiem na
wydajnoci spowodowanym poredni drog kreacji obiektu.
Dalsze usprawnienia
Moemy dalej usprawnia szablon TPair - tak, aby wygoda korzystania z niego nie
ustpowaa niczym przyjemnoci uytkowania typw wbudowanych. Dodamy mu wic:
operator przypisania
konstruktor kopiujcy
Ale po co?, moesz spyta. Przecie w tym przypadku wersje tworzone przez
kompilator pasuj jak ula. Owszem, masz racj. Mona je jednak poprawi, definiujc
obie metody jako szablony:
template <typename T1, typename T2> struct TPair
{
T1 Pierwszy; // warto pierwszego pola
T2 Drugi; // warto drugiego pola
//-------------------------------------------------------------------
// konstruktory (zwyke i kopiujco-konwertujcy)
TPair() : Pierwszy(), Drugi() { }
TPair(const T1& Wartosc1, const T2& Wartosc2)
: Pierwszy(Wartosc1), Drugi(Wartosc2) { }
template <typename U1, typename U2> TPair(const TPair<U1, U2>& Para)
: Pierwszy(Para.Pierwszy), Drugi(Para.Drugi) { }
//-------------------------------------------------------------------
// operator przypisania
template <typename U1, typename U2>
operator=(const TPair<U1, U2>& Para)
{
Pierwszy = Para.Pierwszy;
Drugi = Para.Drugi;
return *this;
}
};
W ten sposb pieczemy dwa befsztyki na jednym ogniu. Nasze metody peni bowiem nie
tylko rol kopiujc, ale i rol konwertujc. Pary staj si wic kompatybilne
wzgldem niejawnym konwersji swoich skadnikw; zatem np. para TPair<int, int>
bdzie moga by od teraz bez problemw przypisana do pary TPair<float, double>,
itd. Konieczne konwersje bd dokonywane podczas inicjalizacji (konstruktor) lub
przypisywania (operator =) pl.
Do peni funkcjonalnoci brakuje jeszcze moliwoci porwnywania par. To za osigamy,
definiujc operatory == i !=. Take tutaj moe zaj konieczno konfrontowania duetw
o rnych typach pl, zatem ponownie naley uy szablonu:
// operator rwnoci
template <typename T1, typename T2, typename U1, typename U2>
inline bool operator==(const TPair<T1, T2>& Para1,
const TPair<U1, U2>& Para2)
{
return (Para1.Pierwszy == Para2.Pierwszy
&& Para1.Drugi == Para2.Drugi);
Zaawansowane C++ 536
}
// operator nierwnoci
template <typename T1, typename T2, typename U1, typename U2>
inline bool operator!=(const TPair<T1, T2>& Para1,
const TPair<U1, U2>& Para2)
{
return (Para1.Pierwszy != Para2.Pierwszy
|| Para1.Drugi != Para2.Drugi);
}
Troch makabrycznie na pierwszy rzut oka moe wyglda szablon z czterema
parametrami. Powd jego wystpienia jest jednak banalny: potrzebujemy po prostu
parametryzacji typw dla obu porwnywanych par. W sumie wic mog wystpi cztery
typy pl, co adnie przedstawiaj deklaracje parametrw funkcji.
O tym, czy typy te bd ze sob wspgray, zdecyduj ju porwnywania w ciele funkcji
operatorowych. Naturalnie, w przypadku braku identycznoci lub niejawnych konwersji,
kompilacji problematycznego uycia operatora nie powiedzie si.
Stworzony szablon TPair wraz z oprzyrzdowaniem w postaci pomocniczej funkcji i
przecionych operatorw jest bardzo podobny do klasy std::pair z Biblioteki
Standardowej.
Trjki i wysze krotki
Przygldajc si uwaniej szablonowi pary, nietrudno jest dostrzec miejsca, ktre naley
zmodyfikowa, by otrzyma krotki wyszego rzdu - trjki, czwrki, pitki, itd. Pewnym
problemem jest stae zwikszanie dugoci klauzul template <...> i nazw typw krotek,
ale to ju jest niestety nieuknione. W praktyce wic rzadko uywa si wielkich krotek -
powyej trzech, czterech elementw - take z tego powodu, e nie ma dla nich zbyt wielu
sensownych zastosowa.
Dlatego te tutaj popatrzymy sobie tylko na analogiczny do TPair szablon trjki -
TTriplet:
template <typename T1, typename T2, typename T3> struct TTriplet
{
T1 Pierwszy; // warto pierwszego pola
T2 Drugi; // warto drugiego pola
T3 Trzeci; // warto trzeciego pola
//-------------------------------------------------------------------
// konstruktory (zwyke i kopiujco-konwertujcy)
TTriplet() : Pierwszy(), Drugi(), Trzeci() { }
TTriplet(const T1& Wartosc1, const T2& Wartosc2, const T3& Wartosc3)
: Pierwszy(Wartosc1), Drugi(Wartosc2), Trzeci(Wartosc3) { }
template <typename U1, typename U2, typename U3>
TTriplet(const TTriplet<U1, U2, U3>& Trojka)
: Pierwszy(Trojka.Pierwszy),
Drugi(Trojka.Drugi), Trzeci(Trojka.Trzeci) { }
//-------------------------------------------------------------------
// operator przypisania
template <typename U1, typename U2, typename U3>
operator=(const TTriplet<U1, U2, U3>& Trojka)
{
Pierwszy = Trojka.Pierwszy;
Szablony 537
Drugi = Trojka.Drugi;
Trzeci = Trojka.Trzeci;
return *this;
}
};
// operator rwnoci
template <typename T1, typename T2, typename T3,
typename U1, typename U2, typename U3>
inline bool operator==(const TTriplet<T1, T2, T3>& Trojka1,
const TTriplet<U1, U2, U3>& Trojka2)
{
return (Trojka1.Pierwszy == Trojka2.Pierwszy
&& Trojka1.Drugi == Trojka2.Drugi
&& Trojka1.Trzeci == Trojka2.Trzeci);
}
// operator nierwnoci
template <typename T1, typename T2, typename T3,
typename U1, typename U2, typename U3>
inline bool operator==(const TTriplet<T1, T2, T3>& Trojka1,
const TTriplet<U1, U2, U3>& Trojka2)
{
return (Trojka1.Pierwszy != Trojka2.Pierwszy
|| Trojka1.Drugi != Trojka2.Drugi
|| Trojka1.Trzeci != Trojka2.Trzeci);
}
// ----------------------------------------------------------------------
// wygodna funkcja tworzca trojk
template <typename T1, typename T2, typename T3>
inline TTriplet<T1, T2, T3> Trojka(const T1& Wartosc1,
const T2& Wartosc2,
const T3& Wartosc3)
{
return TTriplet<T1, T2, T3>(Wartosc1, Wartosc2, Wartosc3);
}
Wyglda on lekko strasznie, ale te pokazuje wyranie, e szablony w C++ to naprawd
potne narzedzie. Pomyl, czy w ogle sensowne byoby implementowanie krotek bez
nich?
Wysze krotki wygodnie jest programowa w sposb rekurencyjny, wykorzystujc jedynie
szablon pary. Przy takim podejciu trjka np. typu TTriplet<int, float,
std::string> jest przechowywana jako typ TPair<int, TPair<float, std::string>
> - czyli par, ktrej elementem jest kolejna para. Analogicznie wyglda to dalej.
Takie podejcie, w poczeniu z kilkoma innymi, maksymalnie wykrconymi technikami,
daje moliwo tworzenia krotek dowolnego rzdu. Takie rozwizanie jest czci znanej
biblioteki Boost.
Pojemniki
Nadesza pora, by pozna gwny powd wprowadzenia do C++ mechanizmu szablonw.
S nim mianowicie klasy kontenerowe.
Zaawansowane C++ 538
Kontenery albo pojemniki (ang. containers) to specjalne struktury danych
przeznaczone do zarzdzania kolekcjami obiektw tego samego typu w okrelony sposb.
Poniewa definicja ta jest bardzo oglna, mamy mnstwo rodzajw kontenerw. Spora
ich cz zostaa zaimplementowana w Bibliotece Standardowej, a o wszystkich mwi
dowolna ksika o algorytmach i strukturach danych.
Nie bdziemy tutaj omawia kadego rodzaju pojemnika, lecz skoncentrujemy si jedynie
na tym, w jaki sposb szablony pomagaj im w prawidowym funkcjonowaniu.
Zobaczymy wic najdoniolejsze zastosowanie szablonw w programowaniu.
Przykad klasy kontenera - stos
Zgodnie ze zwyczajem, kontenery poznamy na przykadzie jednego z prostszych
rodzajw. Bdzie to stos.
Czym jest stos
Pojcie stosu jest ci znane; podczas omawiania wskanikw na funkcje wyjaniem
bowiem, e jest to pomocny obszar pamici, poprzez ktry odbywa si transfer
argumentw od wywoujcego funkcj.
Stos (ang. stack) ma te inne znaczenie. Jest to rodzaj pojemnika przechowujcego
dowolne elementy, charakteryzujcy si tym, i:
obiekty s na stos jedynie odkadane (ang. push) i pobierane (ang. pop)
w danej chwili ma si dostp jedynie do ostatnio pooonego, szczytowego
elementu
obiekty s zdejmowane w odwrotnej kolejnoci ni byy odkadane na stos
Wida wic analogi do stosu - obszaru pamici. Tam obiektami odkadanymi byy
parametry funkcji. Pooone w jednej kolejnoci, musiay by nastpnie podejmowane w
porzdku odwrotnym. Cay czas rwnie dobre jest porwnanie do stosu ksiek: jeli
pooymy na biurku sownik ortograficzny, na nim ksik kucharsk, a na samej grze
podrcznik fizyki, to aby pozna prawidow pisowni sowa gegka bdziemy musieli
wpierw zdj dwie ksiki lece na sowniku. Przy czym najpierw pozbdziemy si
podrcznika, a potem ksiki z przepisami.
Definicja szablonu klasy
Na tej samej zasadzie dziaa stos - struktura danych. Jest to co w rodzaju tablicy,
przechowujcej obiekty dowolnego typu, bdce odoonymi na stos elementami. Nie
pozwala ona jednak na pobranie dowolnego elementu (o ustalonym indeksie), lecz
wymaga zdejmowania obiektw w kolejnoci odwrotnej do porzdku ich odkadania.
Najlepszym sposobem na wprowadzenie stosu do programowania w C++ jest
zdefiniowanie odpowiedniego szablonu klasy. Dziki temu wszystkie szczegy
implementacji zostan ukryte (zaleta OOPu), a nasz stos bdzie potrafi operowa
elementami dowolnych typw (zaleta szablonw).
Spjrzmy wic na propozycj takiego szablonu stosu:
template <typename T, unsigned N> class TStack
{
private:
// zawarto stosu
T m_aStos[N];
// aktualny rozmiar (liczba elementw) stosu
unsigned m_uRozmiar;
public:
Szablony 539
// konstruktor
TStack() : m_uRozmiar(0) { }
//-------------------------------------------------------------
// odoenie elementu na stos
void Push(const T& Element)
{
if (m_uRozmiar == N)
throw "TStack::Push() - stos jest peen";
m_aStos[m_uRozmiar] = Element; // dodanie elementu
++m_uRozmiar; // zwiksz. licznika
}
// pobranie elementu ze szczytu stosu
T Pop()
{
if (m_uRozmiar == 0)
throw "TStack::Pop() - stos jest pusty";
// zwrcenie elementu i zmniejszenie licznika
return m_aStos[--m_uRozmiar];
}
};
Jest to waciwie najprostsza moliwa wersja stosu. Dwa parametry szablonu okrelaj w
niej typ przechowywanych elementw oraz maksymaln ich liczb. Drugi oczywicie nie
jest konieczny - atwo wyobrazi sobie (i napisa) stos, ktry uywa dynamicznej tablicy i
dostosowuje si do liczby odoonych elementw.
Co do metod, to ich garnitur jest rwnie skromny. Metoda Push() powoduje odoenie
na stos podanej wartoci, za Pop() - pobranie jej i zwrcenie w wyniku. To absolutne
minimum; czsto dodaje si do tego jeszcze funkcj Top() (szczyt), ktra zwraca
element lecy na grze bez zdejmowania go ze stosu.
Klas mona te usprawnia dalej: dodajc szablonowy kostruktor kopiujcy i operator
przypisania, metody zwracajce aktualny rozmiar stosu (liczb odoonych elementw) i
inne dodatki. Monaby nawet zmieni wewntrzny mechanizm funkcjonowania klasy i
zaprzc do pracy szablon TArray - dziki temu maksymalny rozmiar stosu mgby by
ustalany dynamicznie.
Zawsze jednak istota dziaania pojemnika bdzie taka sama.
Korzystanie z szablonu
Spoytkowanie tak napisanego stosu nie jest trudne. Oto najbanalniejszy z banalnych
przykadw:
// deklaracja obiektu stosu, zawierajcego maksymalnie 5 liczb typu int
TStack<int, 5> Stos;
// odoenie paru liczb na stos
Stos.Push (12);
Stos.Push (23);
Stos.Push (34);
// podjcie i wywietlenie odoonych liczb
for (unsigned i = 0; i < 3; ++i)
std::cout << Stos.Pop() << std::endl;
W jego rezultacie zobaczylibymy wypisanie liczb:
Zaawansowane C++ 540
34
23
12
Wida zatem wyranie, e metoda Pop() powoduje zwrcenie elementw stosu w
kolejnoci przeciwnej do ich odkadania poprzez Push(). Na tym wanie opiera si idea
stosu.
Stos ma w programowaniu rozliczne zastosowania: poczwszy od rekurencyjnego
przeszukiwania hierarchicznych baz danych (jak chociaby katalogi na dysku twardym)
po rysowanie trjwymiarowych modeli w grach komputerowych. Obok zwykej tablicy,
jest to chyba najczciej wykorzystywany pojemnik.
Programowanie oglne
Szablony, a szczeglnie ich uycie do implementacji kontenerw, stay si podstaw idei
tak zwanego programowania oglnego (ang. general programming). Trudno
precyzyjnie j wyrazi i zdefiniowa, ale mona j rozumie jako poszukiwanie jak
najbardziej abstrakcyjnych i oglnych rozwiza w postaci algorytmw i struktur danych.
Rozwizania powstae w zgodzie z t ide s wic niesychanie elastyczne.
Dobrym przykadem s wanie kontenery. Istnieje wiele ich rodzajw, poczwszy od
prostych tablic jednowymiarowych po zoone struktury, jak np. drzewa. Dla kadego
pojemnika logiczne jest jednak przeprowadzanie pewnych typowych operacji, jak na
przykad wyszukiwanie okrelonego elementu. Operacje te nazywami algorytmami.
Logiczne byoby zaprogramowanie algorytmw jako metod klas kontenerowych.
Rozwizanie to ma jednak wad: poniewa kady pojemnik jest zorganizowany inaczej,
naleaoby dla kadego z nich zapisa osobn wersj algorytmu. Problem ten rozwizano
poprzez dodanie abstrakcyjnego pojcia iteratora - obiektu, ktry suy do przegldania
kontenera. Iterator ukrywa wszelkie szczegy zwizane z konkretnym pojemnikiem,
przez co algorytm oparty na wykorzystaniu iteratorw moe by napisany raz i
wykorzystywany wielokrotnie w odniesieniu do dowolnych kontenerw.
Ten zmylny pomys sta si podstaw stworzenia Standardowej Biblioteki Szablonw
(ang. Standard Template Library - STL). Jest to gwna cz Biblioteki Standardowej
jzyka C++ i zawiera wiele szablonw podstawowych struktur danych. S one wsparte
algorytmami, iteratorami i innymi pomocniczymi pojciami, dziki ktrym STL jest nie
tylko bogata funkcjonalnie, ale i efektywna oraz elastyczna. To jedno z bardziej
uytecznych narzdzi jzyka C++ i jednoczenie najwaniejsze zastosowanie szablonw.
Podsumowanie
Ten rozdzia koczy kurs jzyka C++. Na ostatku zapoznae si z jego najbardziej
zaawansowanym mechanizmem - szablonami.
Wpierw wic zobaczye sytuacje, w ktrych cisa kontrola typw w C++ jest powodem
problemw. Chwil pniej otrzymae te do rki lekarstwo, czyli wanie szablony.
Przeszlimy potem do dokadnego omwienia ich dwch rodzajw: szablonw funkcji i
szablonw klas.
W sposb oglniejszy zajlimy si nimi w nastpnym podrozdziale. Poznae zatem trzy
rodzaje parametrw szablonw, ktre daj im razem bardzo potne waciwoci. Zaraz
jednak uwiadomiem ci take problemy zwizane z szablonami: poczwszy od
koniecznoci udzielania podpowiedzi dla kompilatora co do znaczenia niektrych nazw, a
koczc na kwestii organizacji kodu szablonw w plikach rdowych.
Szablony 541
W trzecim podrozdziale przyjrzelimy si natomiast najbardziej typowym zastosowaniom
szablonw - czyli dowiedzielimy si, jak zdobyta wiedza moe si przyda w praktyce.
Pytania i zadania
Teraz czeka ci jeszcze tylko odpowied na kilka sprawdzajcych wiedz pyta i
wykonanie zada. Powodzenia!
Pytania
1. Co to znaczy, e C++ jest jzykiem o cisej kontroli typw?
2. W jaki sposb mona stworzy oglne funkcje, dziaajce dla wielu typw
danych?
3. Jakie s sposoby na implementacj oglnych klas pojemnikowych bez uycia
szablonw?
4. Jak definiujemy szablon?
5. Jakie rodzaje szablonw s dostpne w C++?
6. Czym jest specjalizacja szablonu? Czym si rni specjalizacja czciowa od
penej?
7. Skd kompilator bierze wartoci (nazwy typw) dla parametrw szablonw
funkcji?
8. Ktre parametry szablonu funkcji mog by wydedukowane z jej wywoania?
9. Co dzieje si, gdy uywamy szablonu funkcji lub klasy? Jakie zadania spoczywaj
wwczas na kompilatorze?
10. Jakie trzy rodzaje parametrw moe posiada szablon klasy?
11. Jaka jest rola sowa kluczowego typename? Gdzie i dlaczego jest ono konieczne?
12. Na czym polega model wczania?
13. Ktry sposb organizacji kodu szablonw najbardziej przypomina tradycyjn
metod podziau kodu w C++?
14. Dlaczego nie naley uywa makrodefinicji w celu imitowania szablonw funkcji?
15. Czym jest krotka?
16. Co rozumiemy pod pojciem pojemnika lub kontenera?
wiczenia
1. Napisz szablon funkcji Suma(), obliczajcy sum wartoci elementw podanej
tablicy TArray.
2. (Trudniejsze) Zdefiniuj szablon klas tablicy wskanikw o nazwie TPtrArray,
dziedziczcy z TArray. Szablon ten powinien przyjmowa jeden parametr, bdcy
typem, na ktry pokazuj elementy tablicy.
3. (Bardzo trudne) Dodaj do specjalizacji TArray<TArray<TYP> > przeciony
operator [], ktry bdzie dziaa w ten sam sposb, jak dla zwykych
wielowymiarowych tablic jzyka C++.
Wskazwka: operator ten bdzie wobec tablicy uywany dwukrotnie. Pomyl wic,
jak warto (obiekt tymczasowy) powinno zwraca jego pierwsze uycie, aby
drugie zwrcio w wyniku dany element tablicy.
4. (Trudniejsze) Opracuj i zaimplementuj algorytm dokonujcy przedstawiania
liczby naturalnej w systemie rzymskim.
Wskazwka: wykorzystaj tablic przegldow par: litera rzymska plus
odpowiadajca jej liczba dziesitna.
5. Napisz szablon TQueue, podobny do TStack, lecz implementujcy pojemnik zwany
kolejk. Kolejk dziaa w ten sposb, i elementy s dodawane do jej pierwszego
koca, natomiast pobierane s z drugiego - tak samo, jak obsugiwane s osoby
stojce w kolejce w sklepie czy banku. Podobnie jak w przypadku stosu, moesz
okreli jej maksymalny rozmiar jako parametr szablonu.
I
INNE
INDEKS
#
# (operator) 319
## (operator) 319
#define (dyrektywa) 307
a const 310
makrodefinicje 317
#elif (dyrektywa) 330
#else (dyrektywa) 327
#endif (dyrektywa) 326
#error (dyrektywa) 330
#if (dyrektywa) 328
#ifdef (dyrektywa) 326
#ifndef (dyrektywa) 327
#include (dyrektywa) 35, 331
wielokrotne doczanie 333
z cudzysowami 332
z nawiasami ostrymi 331
#line (dyrektywa) 315
#pragma (dyrektywa) 334
auto_inline 337
comment 339
deprecated 336
inline_depth 338
inline_recursion 338
message 335
once 339
warning 336
#undef (dyrektywa) 307
_
__alignof (operator) 384
__asm (instrukcja) 246
__cdecl (modyfikator) 285
__cplusplus (makro) 316
__DATE__ (makro) 315
__declspec (modyfikator)
align 384
deprecated 336
property 175
__fastcall (modyfikator) 285
__FILE__ (makro) 315
__int16 (typ) 79
__int32 (typ) 79
__int64 (typ) 79
__int8 (typ) 79
__LINE__ (makro) 314
__stdcall (modyfikator) 285
__TIME__ (makro) 315
__TIMESTAMP__ (makro) 316
A
abort() (funkcja) 453
abs() (funkcja) 94
agregaty
inicjalizacja 356
algorytm 20
aliasy typw 81
alternatywa
bitowa 376
logiczna 106, 377
alternatywa wykluczajca 377
aplikacje
konsolowe 31
okienkowe 31
auto_ptr (klasa) 461
B
BASIC 23
bool (typ) 108
Boost (biblioteka) 487
break (instrukcja) 57, 65
C
callback 295, 438
case (instrukcja) 57
catch (sowo kluczowe) 440
dopasowywanie bloku do wyjtku 444
kolejno blokw 444, 468
stosowanie 442
uniwersalny blok catch 448
cdecl (konwencja wywoania) 285
ceil() (funkcja) 94
cerr (obiekt) 443
char (typ) 79
cigi znakw Patrz acuchy znakw
cin (obiekt) 40
class (sowo kluczowe) 167
na licie parametrw szablonu 510
const (modyfikator) 41, 76
w odniesieniu do metod 175
w odniesieniu do wskanikw 255
const_cast (operator) 261, 386
continue (instrukcja) 66
cos() (funkcja) 91
cout (obiekt) 34
D
default (instrukcja) 57
defined (operator) 328
Inne 546
deklaracje
funkcji 145
zapowiadajce 157
zmiennych 39
delegaci 427
delete (operator) 383
niszczenie obiektw 186
przecianie 409
zwalnianie pamici 269
delete[] (operator) 271, 383
przecianie 409
Delphi 24
dereferencja 256, 381
destruktory 177
a dziedziczenie 203
a wyjtki 455
wirtualne 209
Dev-C++ 27
do (instrukcja) 59
double (modyfikator) 80
double (typ) 80
dynamic_cast (operator) 217, 385
dyrektywy preprocesora 304
dziedziczenie 193
jednokrotne 198
klas szablonowych 492
pojedyncze 198
skadnia w C++ 197
szablonw klas 493
wielokrotne 202
E
else (instrukcja) 53
endl (manipulator) 34
enkapsulacja 229
enum (sowo kluczowe) 125
exit() (funkcja) 454, 456
exp() (funkcja) 89
explicit (sowo kluczowe) 367
export (sowo kluczowe) 526
extern (modyfikator) 157
F
fastcall (konwencja wywoania) 285
float (typ) 80
floor() (funkcja) 94
fmod() (funkcja) 95
for (instrukcja) 63
free() (funkcja) 270
friend (sowo kluczowe) 344
funkcja 36
funkcje
cechy charakterystyczne 282
inline 322
operatorowe 389
parametry 47
prototypy 145
przecianie 94
skadnia 50
wartoci zwracane 49
zwrotne 295, 424
funkcje zaprzyjanione 345
definiowanie wewntrz klasy 347
deklaracje 345
funktory 408
G
getch() (funkcja) 35
H
hermetyzacja 173
I
IDE 27
if (instrukcja) 51
indeksowanie 381
inicjalizacja 355
agregatw 356
lista 357
poprzez konstruktor 356
skadowych klasy 357
typw podstawowych 356
zerowa 479
inline (modyfikator) 322
instrukcje sterujce
ptle 58
warunkowe 51
int (typ) 78
inteligentne wskaniki 405, 460
interfejs uytkownika 141
inynieria oprogramowania 232
iteratory 540
J
Java 25
jzyk kontekstowy 519
jzyk programowania 22
niskiego poziomu 162
wysokiego poziomu 162
K
klasy 166, 169
abstrakcyjne 211
bazowe 193
definicje klas 170
implemetacja 178
pochodne 193
klasy aprzyjanione
deklarowanie 349
klasy wyjtkw 464
uycie dziedziczenia 465
klasy zaprzyjanione 349
Indeks 547
kod wyjcia 454
komentarze 33
kompilacja warunkowa 325
kompilator 22
koniunkcja
bitowa 375
logiczna 106, 377
konkatencja 102
konkretyzacja 478, 484, 499
jawna 525
konsola 31
konstruktory 176
a dziedziczenie 202
cechy 353
definiowanie 353
domylne 204
konwertujce 365
kopiujce 361, 362
kontenery 538
konwencja wywoania 284
konwersje 364
poprzez konstruktor 365
poprzez operator 368
typy oglne i szczeglne 446
krokowy tryb 37
krotki 533
krotno zwizku klas 238
L
liczby pseudolosowe 92
linker 23
lista inicjalizacyjna 357
inicjalizacja skadowych 357
wywoywanie konstruktorw bazowych
358
log() (funkcja) 89
log10() (funkcja) 89
long (modyfikator) 79
long (typ) 80
l-warto 257, 378
acuchy znakw 98
inicjalizacja 101
czenie 102
pobieranie znaku 103
w stylu C 264
M
main() (funkcja) 33
makrodefinicje 316
a szablony funkcji 323, 529
definiowanie 318
niebezpieczestwa 320
operatory 319
rola nawiasw 321
malloc() (funkcja) 270
metaszablony 517
metody 165
czysto wirtualne 210
deklarowanie 168
implementacja 168
prototypy 175
stae 175
statyczne 226
wirtualne 205
metody zaprzyjanione 348
deklarowanie 348
model separacji 526
wsppraca z modelem wczania 527
model wczania 522
modyfikatory 77
N
nawiasy
klamrowe 125
kwadratowe 104
okrge 388
ostre 518
nazwy
dekorowane 286
przesanianie 73
wtrcone 491
zalene 520
negacja
bitowa 375
logiczna 106, 377
new (operator) 382
alokacja pamici 269
przecianie 409
tworzenie obiektw 184
new[] (operator) 271, 382
przecianie 409
notacja wgierska 40
O
obiekty 164
funkcyjne 408
jako wskaniki 184
jako zmienne 180, 182
konkretne 228
narzdziowe 228
tworzenie 168
zasadnicze 228
obsuga bdw 434
oddzielenie rezultatu 436
wywoanie zwrotne 438
zakoczenie programu 439
zwracanie specjalnej wartoci 435
odwijanie stosu 441, 449
ofstream (klasa) 463
OOP 161
operatory 43, 371
arytmetyczne 43, 374
binarne 96, 372
bitowe 375
Inne 548
cechy 371
dekrementacji 45, 374
dereferencji 256
inkrementacji 45, 96, 374
konwersji 368
logiczne 105, 377
czno 373
pobrania adresu 256
porwnania 105, 376
pracujce z pamici 382
priorytety 44, 372
przecianie 370
przypisania 377
rwnoci a przypisania 55
rzutowania 384
strumieniowe 376
ternarny 389
unarne 95, 372
warunkowy 110
wskanikowe 256, 380
wyuskania 129, 185, 257, 387
zasigu 74
P
pami masowa 247
pami operacyjna 246
dynamiczna alokacja 268
paski model 249
pami wirtualna 247
parametry
funkcji 47, 480
szablonu 477, 480
parametry szablonw
pozatypowe 510
szablony parametrw szablonw 514
typy 509
Pascal 24
pascal (konwencja wywoania) 285
ptla 58
nieskoczona 155, 379
PHP 25
plik wymiany 247
pliki nagwkowe 142
paski model pamici 249
pojemniki 538
pola 165
statyczne 226
polimorfizm 211
pow() (funkcja) 88
pne wizanie 207
preprocesor 303
dyrektywy 304
private
(specyfikator dostpu) 171, 196
(specyfikator dziedziczenia) 198
procedura 36
programowanie
obiektowe 161, 191
oglne 540
strukturalne 159
projekt 30
protected
(specyfikator dostpu) 196
(specyfikator dziedziczenia) 198
prototypy funkcji 145
przecianie
funkcji 94
przecianie operatorw 370
binarnych 397
inkrementacji i dekrementacji 396
oglna skadnia 390
poprzez funkcj globaln 393
poprzez funkcj skadow klasy 392
poprzez zaprzyjanion funkcj globaln
394
przypisania 399
unarnych 395
wskazwki 412
wywoania funkcji 407
zarzdzania pamici 409
przecinek 389
przepenienie stosu 250
przesanianie nazw 73
przestrzenie nazw 388
przesunicie bitowe 376
przyja 344
cechy 351
deklaracje 344
zastosowania 352
pseudokod 22
public
(specyfikator dostpu) 171, 196
(specyfikator dziedziczenia) 198
punkt wykonania 37
p-warto 378
R
rand() (funkcja) 61, 91
referencje 277
deklarowanie 278
jako parametry funkcji 279
register (modyfikator) 245
reinterpret_cast (operator) 261, 385
rejestry procesora 244
rekurencja 338
return (instrukcja) 50
rnica symetryczna
bitowa 376
logiczna 377
RTTI 219, 387
r-warto 257, 378
rzutowanie 83
funkcyjne 386
operatory 384
w d hierarchii klas 217
w stylu C 85, 386
S
segmenty pamici operacyjnej 248
sekwencje ucieczki 306
Indeks 549
set_terminate() (funkcja) 453
set_unexpected() (funkcja) 453
SFINAE 486
short (modyfikator) 79
short (typ) 80
signed (modyfikator) 77
sin() (funkcja) 91
singletony 224
size_t (typ) 83
sizeof (operator) 82, 383
specjalizacje szablonw 484
specyfikacja wyjtkw 451
specyfikatory
dostpu do skadowych 171
dziedziczenia 198
sqrt() (funkcja) 88
srand() (funkcja) 61, 92
staa 41
stae 76
deklarowanie 41
jako parametry szablonw 510
static (modyfikator) 75
static_cast (operator) 87, 384
std (przestrze nazw) 35
stdcall (konwencja wywoania) 285
sterta
obszar pamici 250
STL 540
stos
obszar pamici 249
struktura danych 538
string (klasa) 100
length() (metoda) 104
strings Patrz acuchy znakw
struct (sowo kluczowe)
rnica wobec class 171
struktury 128
definiowanie 128, 131
inicjalizacja 130
strumie
wejcia 40
wyjcia 34
switch (instrukcja) 56
system() (funkcja) 153
sytuacje wyjtkowe 433
szablony 476
eksportowane 526
organizacja kodu 522
problem nawiasw ostrych 498, 518
rodzaje 477
skadnia 477
specjalizacje 484
zastosowania 529
szablony funkcji 478
a makrodefinicje 529
dedukcja parametrw 485
definiowanie 478
specjalizacje 481
wywoywanie 484
zakres stosowalnoci 479
szablony klas 487
definiowanie 489
deklaracje przyjani 495
domylne parametry 506
i dziedziczenie 492
i struktury danych 532
implementacja metod 490
konkretyzacja 499
specjalizacja czciowa 504
specjalizacja metody 503
specjalizacja szablonu 501
szablony metod 495
wsppraca z szablonami funkcji 500
wykorzystanie 491, 497
rodowisko programistyczne 27
T
tablice 113
deklarowanie 113
dynamiczne 270
i wskaniki 261
inicjalizacja 115
wielowymiarowe 120
tan() (funkcja) 91
template (sowo kluczowe) 477, 482
terminate() (funkcja) 452, 453
this (sowo kluczowe) 179
thiscall (konwencja wywoania) 285
throw (instrukcja) 440
ponowne rzucenie wyjtku 448
rnice wzgldem break 450
rnice wzgldem return 441, 450
rzucanie wyjtku 441
throw() (deklaracja) 451
time() (funkcja) 92
tokenizacja 519
tokeny 519
trjznakowe sekwencje 305
try (sowo kluczowe) 440
zagniedanie blokw 446
tryb krokowy 37
type_info (struktura) 220
typedef (instrukcja) 81
typeid (operator) 220, 387
typename (sowo kluczowe)
na licie parametrw szablonu 477, 510
przy nazwach zalenych 521
typy polimorficzne 212
typy strukturalne Patrz struktury
typy wyliczeniowe 123
definiowanie 125
zastosowania 127
U
uncaught_exception() (funkcja) 456
unexpected() (funkcja) 452
unie 136
union (sowo kluczowe) 136
unsigned (modyfikator) 77
Inne 550
unsigned (typ) 80
V
virtual (sowo kluczowe)
oznaczenie metody wirtualnej 205
Visual Basic 23
void* (typ) 259
W
wcin (obiekt) 100
wcout (obiekt) 100
wczesne wizanie 207
while (instrukcja) 59, 60
wskaniki 248
i tablice 261
puste 248
wskaniki do funkcji 281
deklarowanie 289
jako argumenty innych funkcji 294
typy 287
wskaniki do skadowych 414
deklaracja wskanika na metod klasy
422
deklarowanie wskanika na pole klasy
418
uycie wskanika na metod klasy 423
uycie wskanika na pole klasy 419
wskaniki do zmiennych
deklarowanie 252
przekazywanie do funkcji 266
spr o gwiazdk 252
stae wskaniki 254
wskaniki do staych 254
wstring (klasa) 100
wyjtki 439
a zarzdzanie zasobami 456
arkana obsugi 466
apanie 442
naduywanie 471
odrzucanie 448
rzucanie 441
specyfikacja 451
wasne klasy dla nich 464
wykorzystanie 463
wykonania punkt 37
wyrwnanie
danych w pamici 384
Z
zasig
globalny 72
lokalny 70
moduowy 72
zasig zmiennych 69
zmienna 39
zmienne
deklaracje zapowiadajce 157
deklarowanie 39
lokalne 71
modyfikatory 75
podstawowe typy 40
statyczne 75
typy 76
zasig (zakres) 69
zwizek
agregacji 236
asocjacji 237
dwukierunkowy 239
dziedziczenia 235
generalizacji-specjalizacji 235
jednokierunkowy 239
przyporzdkowania 237
zawierania si 236
LICENCJA GNU WOLNEJ
DOKUMENTACJI
Tumaczenie GNU Free Documentation License wedug GNU.org.pl
Wersja 1.1
marzec 2002
Copyright (c) 2000 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Zezwala si na kopiowanie i rozpowszechnianie wiernych kopii
niniejszego dokumentu licencyjnego, jednak bez prawa
wprowadzania zmian
0. Preambua
Celem niniejszej licencji jest zagwarantowanie wolnego dostpu do podrcznika, treci
ksiki i wszelkiej dokumentacji w formie pisanej oraz zapewnienie kademu
uytkownikowi swobody kopiowania i rozpowszechniania wyej wymienionych,
z dokonywaniem modyfikacji lub bez, zarwno w celach komercyjnych, jak i nie
komercyjnych. Ponad to Licencja ta pozwala przyzna zasugi autorowi i wydawcy przy
jednoczesnym ich zwolnieniu z odpowiedzialnoci za modyfikacje dokonywane przez
innych.
Niniejsza Licencja zastrzega te, e wszelkie prace powstae na podstawie tego
dokumentu musz nosi cech wolnego dostpu w tym samym sensie co produkt
oryginalny. Licencja stanowi uzupenienie Powszechnej Licencji Publicznej GNU (GNU
General Public License), ktra jest licencj dotyczc wolnego oprogramowania.
Niniejsza Licencja zostaa opracowana z zamiarem zastosowania jej do podrcznikw do
wolnego oprogramowania, poniewa wolne oprogramowanie wymaga wolnej
dokumentacji: wolny program powinien by rozpowszechniany z podrcznikami, ktrych
dotycz te same prawa, ktre wi si z oprogramowaniem. Licencja ta nie ogranicza
si jednak do podrcznikw oprogramowania. Mona j stosowa do rnych
dokumentw tekstowych, bez wzgldu na ich przedmiot oraz niezalenie od tego, czy
zostay opublikowane w postaci ksiki drukowanej. Stosowanie tej Licencji zalecane jest
gwnie w przypadku prac, ktrych celem jest instrukta lub pomoc podrczna.
1. Zastosowanie i definicje
Niniejsza Licencja stosuje si do podrcznikw i innych prac, na ktrych umieszczona jest
pochodzca od waciciela praw autorskich informacja, e dana praca moe by
rozpowszechniana wycznie na warunkach niniejszej Licencji. Uywane poniej sowo
"Dokument" odnosi si bdzie do wszelkich tego typu publikacji. Ich odbiorcy nazywani
bd licencjobiorcami.
"Zmodyfikowana wersja" Dokumentu oznacza wszelkie prace zawierajce Dokument lub
jego cz w postaci dosownej bd zmodyfikowanej i/lub przeoonej na inny jzyk.
"Sekcj drugorzdn" nazywa si dodatek opatrzony odrbnym tytuem lub sekcj
pocztkow Dokumentu, ktra dotyczy wycznie zwizku wydawcw lub autorw
Inne 552
Dokumentu z ogln tematyk Dokumentu (lub zagadnieniami z ni zwizanymi) i nie
zawiera adnych treci bezporednio zwizanych z ogln tematyk (na przykad, jeeli
Dokument stanowi w czci podrcznik matematyki, Sekcja drugorzdna nie moe
wyjania zagadnie matematycznych). Wyej wyjaniany zwizek moe si natomiast
wyraa w aspektach historycznym, prawnym, komercyjnym, filozoficznym, etycznym lub
politycznym.
"Sekcje niezmienne" to takie Sekcje drugorzdne, ktrych tytuy s ustalone jako tytuy
Sekcji niezmiennych w nocie informujcej, e Dokument zosta opublikowany na
warunkach Licencji.
"Tre okadki" to pewne krtkie fragmenty tekstu, ktre w nocie informujcej, e
Dokument zosta opublikowany na warunkach Licencji, s opisywane jako "do
umieszczenia na przedniej okadce" lub "do umieszczenia na tylnej okadce".
"Jawna" kopia Dokumentu oznacza kopi czyteln dla komputera, zapisan w formacie,
ktrego specyfikacja jest publicznie dostpna. Zawarto tej kopii moe by ogldana
i edytowana bezporednio za pomoc typowego edytora tekstu lub (w przypadku
obrazw zoonych z pikseli) za pomoc typowego programu graficznego lub
(w przypadku rysunkw) za pomoc oglnie dostpnego edytora rysunkw. Ponadto
kopia ta stanowi odpowiednie dane wejciowe dla programw formatujcych tekst lub dla
programw konwertujcych do rnych formatw odpowiednich dla programw
formatujcych tekst. Kopia speniajca powysze warunki, w ktrej jednak zostay
wstawione znaczniki majce na celu utrudnienie dalszych modyfikacji przez czytelnikw,
nie jest Jawna. Kopi, ktra nie jest "Jawna", nazywa si "Niejawn".
Przykadowe formaty kopii Jawnych to: czysty tekst ASCII bez znacznikw, format
wejciowy Texinfo, format wejciowy LaTeX, SGML lub XML wykorzystujce publicznie
dostpne DTD, standardowy prosty HTML przeznaczony do rcznej modyfikacji. Formaty
niejawne to na przykad PostScript, PDF, formaty wasne, ktre mog by odczytywane
i edytowane jedynie przez wasne edytory tekstu, SGML lub XML, dla ktrych DTD i/lub
narzdzia przetwarzajce nie s oglnie dostpne, oraz HTML wygenerowany maszynowo
przez niektre procesory tekstu jedynie w celu uzyskania danych wynikowych.
"Strona tytuowa" oznacza, w przypadku ksiki drukowanej, sam stron tytuow oraz
kolejne strony zawierajce informacje, ktre zgodnie z t Licencj musz pojawi si na
stronie tytuowej. W przypadku prac w formatach nieposiadajcych strony tytuowej
"Strona tytuowa" oznacza tekst pojawiajcy si najbliej tytuu pracy, poprzedzajcy
pocztek tekstu gwnego.
2. Kopiowanie dosowne
Licencjobiorca moe kopiowa i rozprowadza Dokument komercyjnie lub niekomercyjnie,
w dowolnej postaci, pod warunkiem zamieszczenia na kadej kopii Dokumentu treci
Licencji, informacji o prawie autorskim oraz noty mwicej, e do Dokumentu ma
zastosowanie niniejsza Licencja, a take pod warunkiem nie umieszczania adnych
dodatkowych ogranicze, ktre nie wynikaj z Licencji. Licencjobiorca nie ma prawa
uywa adnych technicznych metod pomiarowych utrudniajcych lub kontrolujcych
czytanie lub dalsze kopiowanie utworzonych i rozpowszechnianych przez siebie kopii.
Moe jednak pobiera opaty za udostpnianie kopii. W przypadku dystrybucji duej
liczby kopii Licencjobiorca jest zobowizany przestrzega warunkw wymienionych
w punkcie 3.
Licencjobiorca moe take wypoycza kopie na warunkach opisanych powyej, a take
wystawia je publicznie.
Licencja GNU Wolnej Dokumentacji 553
3. Kopiowanie ilociowe
Jeeli Licencjobiorca publikuje drukowane kopie Dokumentu w liczbie wikszej ni 100,
a licencja Dokumentu wymaga umieszczenia Treci okadki, naley doczy kopie
okadek, ktre zawieraj ca wyran i czyteln Tre okadki: tre przedniej okadki,
na przedniej okadce, a tre tylnej okadki, na tylnej okadce. Obie okadki musz te
jasno i czytelnie informowa o Licencjobiorcy jako wydawcy tych kopii. Okadka przednia
musi przedstawia peny tytu; wszystkie sowa musz by rwnie dobrze widoczne
i czytelne. Licencjobiorca moe na okadkach umieszcza take inne informacje
dodatkowe. Kopiowanie ze zmianami ograniczonymi do okadek, dopki nie narusza
tytuu Dokumentu i spenia opisane warunki, moe by traktowane pod innymi wzgldami
jako kopiowanie dosowne.
Jeeli napisy wymagane na ktrej z okadek s zbyt obszerne, by mogy pozosta
czytelne po ich umieszczeniu, Licencjobiorca powinien umieci ich pocztek(tak ilo,
jaka wydaje si rozsdna) na rzeczywistej okadce, a pozosta cz na ssiednich
stronach.
W przypadku publikowania lub rozpowszechniania Niejawnych kopii Dokumentu w liczbie
wikszej ni 100, Licencjobiorca zobowizany jest albo doczy do kadej z nich Jawn
kopi czyteln dla komputera, albo wymieni w lub przy kadej kopii Niejawnej publicznie
dostpn w sieci komputerowej lokalizacj penej kopii Jawnej Dokumentu, bez adnych
informacji dodanych -- lokalizacj, do ktrej kady uytkownik sieci miaby bezpatny
anonimowy dostp za pomoc standardowych publicznych protokow sieciowych.
W przypadku drugim Licencjobiorca musi podj odpowiednie rodki ostronoci, by
wymieniona kopia Jawna pozostaa dostpna we wskazanej lokalizacji przynajmniej przez
rok od momentu rozpowszechnienia ostatniej kopii Niejawnej (bezporedniego lub przez
agentw albo sprzedawcw) danego wydania.
Zaleca si, cho nie wymaga, aby przed rozpoczciem rozpowszechniania duej liczby
kopii Dokumentu, Licencjobiorca skontaktowa si z jego autorami celem uzyskania
uaktualnionej wersji Dokumentu.
4. Modyfikacje
Licencjobiorca moe kopiowa i rozpowszechnia Zmodyfikowan wersj Dokumentu na
zasadach wymienionych powyej w punkcie 2 i 3 pod warunkiem cisego przestrzegania
niniejszej Licencji. Zmodyfikowana wersja peni wtedy rol Dokumentu, a wic Licencja
dotyczca modyfikacji i rozpowszechniania Zmodyfikowanej wersji przenoszona jest na
kadego, kto posiada jej kopi. Ponadto Licencjobiorca musi w stosunku do
Zmodyfikowanej wersji speni nastpujce wymogi:
A. Uy na Stronie tytuowej (i na okadkach, o ile istniej) tytuu innego ni tytu
Dokumentu i innego ni tytuy poprzednich wersji (ktre, o ile istniay, powinny
zosta wymienione w Dokumencie, w sekcji Historia). Tytuu jednej z ostatnich
wersji Licencjobiorca moe uy, jeeli jej wydawca wyrazi na to zgod.
B. Wymieni na Stronie tytuowej, jako autorw, jedn lub kilka osb albo jednostek
odpowiedzialnych za autorstwo modyfikacji Zmodyfikowanej wersji, a take
przynajmniej piciu spord pierwotnych autorw Dokumentu (wszystkich, jeli
byo ich mniej ni piciu).
C. Umieci na Stronie tytuowej nazw wydawcy Zmodyfikowanej wersji.
D. Zachowa wszelkie noty o prawach autorskich zawarte w Dokumencie.
E. Doda odpowiedni not o prawach autorskich dotyczcych modyfikacji obok
innych not o prawach autorskich.
Inne 554
F. Bezporednio po notach o prawach autorskich, zamieci not licencyjn
zezwalajc na publiczne uytkowanie Zmodyfikowanej wersji na zasadach
niniejszej Licencji w postaci podanej w Zaczniku poniej.
G. Zachowa w nocie licencyjnej pen list Sekcji niezmiennych i wymaganych
Treci okadki podanych w nocie licencyjnej Dokumentu.
H. Doczy niezmienion kopi niniejszej Licencji.
I. Zachowa sekcj zatytuowan "Historia" oraz jej tytu i doda do niej informacj
dotyczc przynajmniej tytuu, roku publikacji, nowych autorw i wydawcy
Zmodyfikowanej wersji zgodnie z danymi zamieszczonymi na Stronie tytuowej.
Jeeli w Dokumencie nie istnieje sekcja pod tytuem "Historia", naley j
utworzy, podajc tytu, rok, autorw i wydawc Dokumentu zgodnie z danymi
zamieszczonymi na stronie tytuowej, a nastpnie dodajc informacj dotyczc
Zmodyfikowanej wersji, jak opisano w poprzednim zdaniu.
J. Zachowa wymienion w Dokumencie (jeli taka istniaa) informacj o lokalizacji
sieciowej, publicznie dostpnej Jawnej kopii Dokumentu, a take o podanych
w Dokumencie lokalizacjach sieciowych poprzednich wersji, na ktrych zosta on
oparty. Informacje te mog si znajdowa w sekcji "Historia". Zezwala si na
pominicie lokalizacji sieciowej prac, ktre zostay wydane przynajmniej cztery
lata przed samym Dokumentem, a take tych, ktrych pierwotny wydawca wyraa
na to zgod.
K. W kadej sekcji zatytuowanej "Podzikowania" lub "Dedykacje" zachowa tytu
i tre, oddajc rwnie ton kadego z podzikowa i dedykacji.
L. Zachowa wszelkie Sekcje niezmienne Dokumentu w niezmienionej postaci
(dotyczy zarwno treci, jak i tytuu). Numery sekcji i rwnowane im oznaczenia
nie s traktowane jako nalece do tytuw sekcji.
M. Usun wszelkie sekcje zatytuowane "Adnotacje". Nie musz one by zaczane
w Zmodyfikowanej wersji.
N. Nie nadawa adnej z istniejcych sekcji tytuu "Adnotacje" ani tytuu
pokrywajcego si z jakkolwiek Sekcj niezmienn.
Jeeli Zmodyfikowana wersja zawiera nowe sekcje pocztkowe lub dodatki stanowice
Sekcje drugorzdne i nie zawierajce materiau skopiowanego z Dokumentu,
Licencjobiorca moe je lub ich cz oznaczy jako sekcje niezmienne. W tym celu musi
on doda ich tytuy do listy Sekcji niezmiennych zawartej w nocie licencyjnej
Zmodyfikowanej wersji. Tytuy te musz by rne od tytuw pozostaych sekcji.
Licencjobiorca moe doda sekcj "Adnotacje", pod warunkiem, e nie zawiera ona
adnych treci innych ni adnotacje dotyczce Zmodyfikowanej wersji -- mog to by na
przykad stwierdzenia o recenzji koleeskiej albo o akceptacji tekstu przez organizacj
jako autorytatywnej definicji standardu.
Na kocu listy Treci okadki w Zmodyfikowanej wersji, Licencjobiorca moe doda
fragment "do umieszczenia na przedniej okadce" o dugoci nie przekraczajcej piciu
sw, a take fragment o dugoci do 25 sw "do umieszczenia na tylnej okadce". Przez
kad jednostk (lub na mocy ustale przez ni poczynionych) moe zosta dodany tylko
jeden fragment z przeznaczeniem na przedni okadk i jeden z przeznaczeniem na tyln.
Jeeli Dokument zawiera ju tre okadki dla danej okadki, dodan uprzednio przez
Licencjobiorc lub w ramach ustale z jednostk, w imieniu ktrej dziaa Licencjobiorca,
nowa tre okadki nie moe zosta dodana. Dopuszcza si jednak zastpienie
poprzedniej treci okadki now pod warunkiem wyranej zgody poprzedniego wydawcy,
od ktrego stara tre pochodzi.
Niniejsza Licencja nie oznacza, i autor (autorzy) i wydawca (wydawcy) wyraaj zgod
na publiczne uywanie ich nazwisk w celu zapewnienia autorytetu jakiejkolwiek
Zmodyfikowanej wersji.
Licencja GNU Wolnej Dokumentacji 555
5. czenie dokumentw
Licencjobiorca moe czy Dokument z innymi dokumentami wydanymi na warunkach
niniejszej Licencji, na warunkach podanych dla wersji zmodyfikowanych w czci 4
powyej, jednak tylko wtedy, gdy w poczeniu zostan zawarte wszystkie Sekcje
niezmienne wszystkich oryginalnych dokumentw w postaci niezmodyfikowanej i gdy
bd one wymienione jako Sekcje niezmienne poczenia w jego nocie licencyjnej.
Poczenie wymaga tylko jednej kopii niniejszej Licencji, a kilka identycznych Sekcji
niezmiennych moe zosta zastpionych jedn. Jeeli istnieje kilka Sekcji niezmiennych
o tym samym tytule, ale rnej zawartoci, Licencjobiorca jest zobowizany uczyni tytu
kadej z nich unikalnym poprzez dodanie na jego kocu, w nawiasach, nazwy
oryginalnego autora lub wydawcy danej sekcji, o ile jest znany, lub unikalnego numeru.
Podobne poprawki wymagane s w tytuach sekcji na licie Sekcji niezmiennych w nocie
licencyjnej poczenia.
W poczeniu Licencjobiorca musi zawrze wszystkie sekcje zatytuowane "Historia"
z dokumentw oryginalnych, tworzc jedn sekcj "Historia". Podobnie ma postpi
z sekcjami "Podzikowania" i "Dedykacje". Wszystkie sekcje zatytuowane "Adnotacje"
naley usun.
6. Zbiory dokumentw
Licencjobiorca moe utworzy zbir skadajcy si z Dokumentu i innych dokumentw
wydanych zgodnie z niniejsz Licencj i zastpi poszczeglne kopie Licencji pochodzce
z tych dokumentw jedn kopi doczon do zbioru, pod warunkiem zachowania zasad
Licencji dotyczcych kopii dosownych we wszelkich innych aspektach kadego
z dokumentw.
Z takiego zbioru Licencjobiorca moe wyodrbni pojedynczy dokument i
rozpowszechnia go niezalenie na zasadach niniejszej Licencji, pod warunkiem
zamieszczenia w wyodrbnionym dokumencie kopii niniejszej Licencji oraz zachowania
zasad Licencji we wszystkich aspektach dotyczcych dosownej kopii tego dokumentu.
7. Zestawienia z pracami niezalenymi
Kompilacja Dokumentu lub jego pochodnych z innymi oddzielnymi i niezalenymi
dokumentami lub pracami nie jest uznawana za Zmodyfikowan wersj Dokumentu,
chyba e odnosz si do niej jako do caoci prawa autorskie. Taka kompilacja jest
nazywana zestawieniem, a niniejsza Licencja nie dotyczy samodzielnych prac
skompilowanych z Dokumentem, jeli nie s to pochodne Dokumentu.
Jeeli do kopii Dokumentu odnosz si wymagania dotyczce Treci okadki wymienione
w czci 3 i jeeli Dokument stanowi mniej ni jedn czwart caoci zestawienia, Tre
okadki Dokumentu moe by umieszczona na okadkach zamykajcych Dokument
w obrbie zestawienia. W przeciwnym razie Tre okadki musi si pojawi na okadkach
caego zestawienia.
Inne 556
8. Tumaczenia
Tumaczenie jest uznawane za rodzaj modyfikacji, a wic Licencjobiorca moe
rozpowszechnia tumaczenia Dokumentu na zasadach wymienionych w punkcie 4.
Zastpienie Sekcji niezmiennych ich tumaczeniem wymaga specjalnej zgody wacicieli
prawa autorskiego. Dopuszcza si jednak zamieszczanie tumacze wybranych lub
wszystkich Sekcji niezmiennych obok ich wersji oryginalnych. Podanie tumaczenia
niniejszej Licencji moliwe jest pod warunkiem zamieszczenia take jej oryginalnej wersji
angielskiej. W przypadku niezgodnoci pomidzy zamieszczonym tumaczeniem
a oryginaln wersj angielsk niniejszej Licencji moc prawn ma oryginalna wersja
angielska.
9. Wyganicie
Poza przypadkami jednoznacznie dopuszczonymi na warunkach niniejszej Licencji nie
zezwala si Licencjobiorcy na kopiowanie, modyfikowanie, czy rozpowszechnianie
Dokumentu ani te na cedowanie praw licencyjnych. We wszystkich pozostaych
wypadkach kada prba kopiowania, modyfikowania lub rozpowszechniania Dokumentu
albo cedowania praw licencyjnych jest niewana i powoduje automatyczne wyganicie
praw, ktre licencjobiorca naby z tytuu Licencji. Niemniej jednak w odniesieniu do stron,
ktre ju otrzymay od Licencjobiorcy kopie albo prawa w ramach niniejszej Licencji,
licencje nie zostan anulowane, dopki strony te w peni si do nich stosuj.
10. Przysze wersje Licencji
W miar potrzeby Free Software Foundation moe publikowa nowe poprawione wersje
GNU Free Documenation License. Wersje te musz pozostawa w duchu podobnym do
wersji obecnej, cho mog si rni w szczegach dotyczcych nowych problemw czy
zagadnie. Patrz http://www.gnu.org/copyleft/. Kadej wersji niniejszej Licencji nadaje
si wyrniajcy j numer. Jeeli w Dokumencie podaje si numer wersji Licencji,
oznaczajcy, i odnosi si do niego podana "lub jakakolwiek pniejsza" wersja licencji,
Licencjobiorca ma do wyboru stosowa si do postanowie i warunkw albo tej wersji,
albo ktrejkolwiek wersji pniejszej opublikowanej oficjalnie (nie jako propozycja) przez
Free Software Foundation. Jeli Dokument nie podaje numeru wersji niniejszej Licencji,
Licencjobiorca moe wybra dowoln wersj kiedykolwiek opublikowan (nie jako
propozycja) przez Free Software Foundation.
Zacznik: Jak zastosowa t Licencj do swojego
dokumentu?
Aby zastosowa t Licencj w stosunku do dokumentu swojego autorstwa, docz kopi
Licencji do dokumentu i zamie nastpujc informacj o prawach autorskich i uwagi o
licencji bezporednio po stronie tytuowej.
Copyright (c) ROK TWOJE IMIE I NAZWISKO
Udziela si zezwolenia do kopiowania rozpowszechniania i/lub modyfikacj tego dokumentu zgodnie z
zasadami Licencji GNU Wolnej Dokumentacji w wersji 1.1 lub dowolnej pniejszej opublikowanej przez
Free Software Foundation; wraz z zawartymi Sekcjami Niezmiennymi LISTA TYTUW SEKCJI, wraz z
Tekstem na Przedniej Okadce LISTA i z Tekstem na Tylnej Okadce LISTA. Kopia licencji zaczona jest
w sekcji zatytuowanej "GNU Free Documentation License"
Licencja GNU Wolnej Dokumentacji 557
Jeli nie zamieszczasz Sekcji Niezmiennych, napisz "nie zawiera Sekcji Niezmiennych"
zamiast spisu sekcji niezmiennych. Jeli nie umieszczasz Teksu na Przedniej Okadce
wpisz "bez Tekstu na Okadce" w miejsce "wraz z Tekstem na Przedniej Okadce LISTA",
analogicznie postp z "Tekstem na Tylnej Okadce"
Jeli w twoim dokumencie zawarte s nieszablonowe przykady kodu programu, zalecamy
aby take uwolni te przykady wybierajc licencj wolnego oprogramowania, tak jak
Powszechna Licencja Publiczna GNU, w celu zapewnienia moliwoci ich uycia w wolnym
oprogramowaniu.