Zaawansowane CPP/Wykład 10: Inteligentne wskaźniki: Różnice pomiędzy wersjami
Matiunreal (dyskusja | edycje) |
Matiunreal (dyskusja | edycje) |
||
Linia 268: | Linia 268: | ||
Poza kontrolą rodzaju praw własności, inteligentny wskaźnik daje nam | Poza kontrolą rodzaju praw własności, inteligentny wskaźnik daje nam | ||
możliwość kontroli nad operacjami dostępu do wskazywanego obiektu | możliwość kontroli nad operacjami dostępu do wskazywanego obiektu | ||
poprzez operatory | poprzez operatory <tt>operator->()</tt> i <tt>operator*()</tt>. Wpływać na | ||
zachowanie tych operatorów możemy dwojako: po pierwsze w oczywisty | zachowanie tych operatorów możemy dwojako: po pierwsze w oczywisty | ||
sposób możemy wykonać dodatkowy kod zanim zwrócimy z nich odpowiednią | sposób możemy wykonać dodatkowy kod zanim zwrócimy z nich odpowiednią | ||
wartość: | wartość: | ||
T &operator*() { | T &operator*() { | ||
<i>zrob coś</i> | |||
return *_p; | return *_p; | ||
} | } | ||
Ten dodatkowy kod może np. sprawdzać czy wskaźnik | Ten dodatkowy kod może np. sprawdzać czy wskaźnik <tt>_p</tt> nie jest | ||
zerowy, może zliczać wywołania, itp. | zerowy, może zliczać wywołania, itp. | ||
Po drugie możemy zmienić zwracany typ. Wbudowane operatory | Po drugie możemy zmienić zwracany typ. Wbudowane operatory <tt>*</tt> i | ||
<tt>-></tt> zwracają odpowiednio <tt>T&</tt> i <tt>T*</tt>. Oczywiście my możemy | |||
zwrócić cokolwiek, ale żeby to miało jakiś sens powinny to być obiekty | zwrócić cokolwiek, ale żeby to miało jakiś sens powinny to być obiekty | ||
zachowujące sie jak | zachowujące sie jak <tt>T&</tt> i <tt>T*</tt>. Takie obiekty które "zachowują | ||
się jak" coś, ale nie są tym (kwacze jak kaczka, ale to nie jest | się jak" coś, ale nie są tym (kwacze jak kaczka, ale to nie jest | ||
kaczka) nazywamy obiektami zastępczymi (proxy). | kaczka) nazywamy obiektami zastępczymi (proxy). | ||
Linia 294: | Linia 294: | ||
Typowe zastosowanie to implemenatacja operacji przypisania do obiektów | Typowe zastosowanie to implemenatacja operacji przypisania do obiektów | ||
które tak naprawdę obiektami nie są. Weźmy jako przykład | które tak naprawdę obiektami nie są. Weźmy jako przykład | ||
<tt>ostream_iterator</tt> dostarczany przez STL, który zezwala traktować | |||
plik wyjściowy jak kontener z iteratorem typu | plik wyjściowy jak kontener z iteratorem typu <tt>OutputIterator</tt>: | ||
vector<int> V(10,7); | vector<int> V(10,7); | ||
copy(V.begin(), V.end(), ostream_iterator<int>(cout, "")); | copy(V.begin(), V.end(), ostream_iterator<int>(cout, "")); | ||
Przypatrzny się temu przykładowi bliżej. Jeśli zdefiniujemy | Przypatrzny się temu przykładowi bliżej. Jeśli zdefiniujemy | ||
ostream_iterator<int>(cout, "") iout; | ostream_iterator<int>(cout, "") iout; | ||
to w zasadzie jedyną dozwoloną operacja jest przypisanie i zwiekszenie | to w zasadzie jedyną dozwoloną operacja jest przypisanie i zwiekszenie | ||
następujące po sobie: | następujące po sobie: | ||
(*iout) <nowiki>=</nowiki> 666; ++iout; | (*iout) <nowiki>=</nowiki> 666; ++iout; | ||
Ewidentnie nie istnieje żaden obiekt do którego referencje moglibyśmy | Ewidentnie nie istnieje żaden obiekt do którego referencje moglibyśmy | ||
Linia 313: | Linia 313: | ||
definiował operator przypisania: | definiował operator przypisania: | ||
class writing_proxy { | class writing_proxy { | ||
std::ostream &_out; | std::ostream &_out; | ||
public: | public: | ||
writing_proxy(std::ostream &out) :_out(out) {}; | writing_proxy(std::ostream &out) :_out(out) {};<br> | ||
void operator<nowiki>=</nowiki>(const T &val) { | |||
_out<<val; | |||
} | |||
}; | |||
Tę klasę zamkniemy wewnątrz klasy <tt>ostream_iterator</tt> | |||
template<typename T> class ostream_iterator: | |||
public std::iterator <std::output_iterator_tag, T > { | |||
class writing_proxy { | |||
public std:: | <i>...</i> | ||
};<br> | |||
std::string _sep; | |||
std::ostream &_out; | |||
writing_proxy _proxy; | |||
public: | |||
ostream_iterator(std::ostream &out,std::string sep): | |||
_out(out),_sep(sep),_proxy(_out){}; | |||
void operator++() {_out<<_sep;} | |||
void operator++(int) {_out<<_sep;} | |||
writing_proxy &operator*() {return _proxy;}; | |||
}; | |||
Dziedziczenie z klasy <tt>iterator</tt> zapewni nam, że nasz | |||
<tt>ostream_iterator</tt> posiada wszystkie typy stowarzyszone wymagane | |||
Dziedziczenie z klasy | |||
przez iteratory STL. To z kolei pociąga za soba możliwość użycia | przez iteratory STL. To z kolei pociąga za soba możliwość użycia | ||
<tt>iterator_traits</tt><font color=red>(zobacz {[##lbl:iterator-traits|Uzupelnic lbl:iterator-traits|]})</font>. Bez tego nie moglibyśmy używać <tt>ostream_iterator</tt> w niektórych algorytmach STL. | |||
moglibyśmy używać | |||
Teraz możemy juz używać wyrażeń typu: | Teraz możemy juz używać wyrażeń typu: | ||
ostream_iterator<int> io(std::cout,""); | ostream_iterator<int> io(std::cout,""); | ||
(*io)<nowiki>=</nowiki>44; | (*io)<nowiki>=</nowiki>44; | ||
Wywołanie | Wywołanie <tt>*io</tt> zwraca <tt>writing_proxy</tt>. Następnie wywoływany | ||
jest | jest | ||
writing_proxy::operator<nowiki>=</nowiki>(44)} | writing_proxy::operator<nowiki>=</nowiki>(44)} | ||
który wykonuje operację | który wykonuje operację | ||
std::cout<<44; | std::cout<<44; | ||
Widać też że operacja | Widać też że operacja | ||
i<nowiki>=</nowiki>(*io) | i<nowiki>=</nowiki>(*io) | ||
się nie powiedzie (nie skompiluje). W tym przypadku jest to pożądane | się nie powiedzie (nie skompiluje). W tym przypadku jest to pożądane | ||
zachowanie,q bo taka operacja nie ma sensu. | zachowanie,q bo taka operacja nie ma sensu. Jeśli byśmy jednak | ||
chcieli umożliwić działanie operacji przypisania w drugą stronę możemy | chcieli umożliwić działanie operacji przypisania w drugą stronę możemy | ||
w obiekcie proxy zdefiniować operator konwersji na typ | w obiekcie proxy zdefiniować operator konwersji na typ <tt>T</tt>. | ||
operator T() {return T();}; | operator T() {return T();}; <i>uwaga bzdurny przykład !!!</i> | ||
Wtedy wykonanie | Wtedy wykonanie | ||
i<nowiki>=</nowiki>(*io) | i<nowiki>=</nowiki>(*io) | ||
przypisze zero do zmiennej | przypisze zero do zmiennej <tt>i</tt>. W ten sposób obiekty proxy | ||
pozwalają nam rozróżniać użycie operatora | pozwalają nam rozróżniać użycie operatora <tt>*</tt> do odczytu i do | ||
zapisu. | zapisu. | ||
Obiekty zastępcze stanowią zresztą często spotykany wzorzec projektowy | Obiekty zastępcze stanowią zresztą często spotykany wzorzec projektowy | ||
(zobacz | (zobacz STL). Poniżej przedstawię jeszcze jedną "sztuczkę" | ||
opisaną w | opisaną w Alexandrescu: <i>"Nowoczesne Projektowanie"</i> służącą do automatycznego obudowywania | ||
funkcji wywoływanych za pośrednictwem inteligentnego wskaźnika | funkcji wywoływanych za pośrednictwem inteligentnego wskaźnika | ||
wywoływanami innych funkcji. | wywoływanami innych funkcji. | ||
Linia 392: | Linia 389: | ||
Załóżmy że mamy obiekt typu | Załóżmy że mamy obiekt typu | ||
struct Widget { | struct Widget { | ||
void pre() {cout<<"pre"<<endl;}; | void pre() {cout<<"pre"<<endl;}; | ||
void post() {cout<<"post"<<endl;}; | void post() {cout<<"post"<<endl;};<br> | ||
void f1() {cout<<"f1"<<endl;} | |||
void f1() {cout<<"f1"<<endl;} | void f2() {cout<<"f2"<<endl;} | ||
void f2() {cout<<"f2"<<endl;} | } | ||
} | |||
Niech | Niech | ||
Smart_prt<Widget> pw(new Widget); | Smart_prt<Widget> pw(new Widget); | ||
bedzię inteligentnym wskaźnikiem do | bedzię inteligentnym wskaźnikiem do <tt>Widget</tt>. | ||
Chcemy aby każde wywołanie funkcji z | Chcemy aby każde wywołanie funkcji z <tt>Widget</tt> np. | ||
pw->f1() | pw->f1() | ||
zostało poprzedzone poprzez wywołanie funkcji | zostało poprzedzone poprzez wywołanie funkcji <tt>pre()</tt>, a po nim | ||
nastapiło wywołanie funkcji | nastapiło wywołanie funkcji <tt>post()</tt>. Jedną z możliwości jest | ||
oczywiście zmiana kodu funkcji | oczywiście zmiana kodu funkcji <tt>f?</tt>, tak aby wywoływały na początku | ||
<tt>pre()</tt> i <tt>post()</tt> na końcu. Można też dodać zestaw funkcji | |||
opakowywujących: | opakowywujących: | ||
f1_wrapper() {pre();f1();post();} | f1_wrapper() {pre();f1();post();} | ||
Jest to jednak niepotrzebne duplikowanie kodu i możliwe do | Jest to jednak niepotrzebne duplikowanie kodu i możliwe do | ||
zastosowania tylko jeśli mamy możliwość zmiany kodu klasy | zastosowania tylko jeśli mamy możliwość zmiany kodu klasy <tt>Widget</tt>. | ||
Można jednak zrobić inaczej. Zdefiniujemy pomocniczą klasę | Można jednak zrobić inaczej. Zdefiniujemy pomocniczą klasę | ||
template<typename T> struct Wrapper { | template<typename T> struct Wrapper { | ||
T* _p; | T* _p; | ||
Wrapper(T* p):_p(p) {_p->pre();} | Wrapper(T* p):_p(p) {_p->pre();} | ||
~Wrapper() {_p->post();}<br> | |||
T* operator->() {return _p;} | |||
}; | |||
W klasie inteligentnego wskaźnika przedefiniujemy <tt>operator->()</tt> | |||
tak, aby zwracał <tt>Wrapper<T>(T *)</tt> zamiast<tt>T*</tt>. | |||
template<typename T> struct Smart_ptr { | |||
T *_p; | |||
Smart_ptr(T *p):_p(p) {}; | |||
template<typename T> struct Smart_ptr { | ~Smart_ptr(){delete _p;};<br> | ||
T *_p; | Wrapper<T> operator->() {return Wrapper<T>(_p);}; | ||
Smart_ptr(T *p):_p(p) {}; | T &operator*() {return *_p}; | ||
}; | |||
Wrapper<T> operator->() {return Wrapper<T>(_p);}; | |||
T &operator*() {return *_p}; | |||
}; | |||
Jeśli teraz wywołamy | Jeśli teraz wywołamy | ||
pw->f1(); | pw->f1(); | ||
to bedą się dziać następujace rzeczy: | to bedą się dziać następujace rzeczy: | ||
* zostanie wywołany | * zostanie wywołany | ||
pw.operator->(); | pw.operator->(); | ||
operator ten zwraca obiekt | operator ten zwraca obiekt <tt>tmp</tt> typu <tt>Wrapper<Widget></tt> ale | ||
najpier musi go skonstruować, a więc | najpier musi go skonstruować, a więc | ||
* zostanie wywołany konstruktor | * zostanie wywołany konstruktor | ||
tmp<nowiki>=</nowiki>Wrapper<Widget>(p); | tmp<nowiki>=</nowiki>Wrapper<Widget>(p); | ||
który wywoła | który wywoła | ||
p->pre(); | p->pre(); | ||
* Jeśli | |||
* Jeśli <tt>operator->()</tt> zwróci obiekt który posiada <tt>operator->()</tt> to jest on wywoływany rekurencyjnie, aż zostanie zwrócony typ wskaźnikowy. Tak więc zostanie wywołany <tt>tmp.operator->()</tt> który zwroci <tt>p</tt>. | |||
zwrócony typ wskaźnikowy. Tak więc zostanie wywołany | |||
* Poprzez ten wskaźnik zostanie wywołana funkcja | * Poprzez ten wskaźnik zostanie wywołana funkcja | ||
p->f1(); | p->f1(); | ||
* Zakończy się wywołanie | |||
wywołany destruktor obiektu tymczasowego | * Zakończy się wywołanie <tt>pw.operator->()</tt> a więc zostanie wywołany destruktor obiektu tymczasowego <tt>tmp</tt> który wywoła | ||
p->post(); | p->post(); | ||
Widać więc, że w końcu zostanie wykonana sekwencja wywołań: | Widać więc, że w końcu zostanie wykonana sekwencja wywołań: | ||
p->pre(); | p->pre(); | ||
p->f1(); | p->f1(); | ||
p->post(); | p->post(); | ||
i tak bedzie dla dowolnej wywoływanej metody. | i tak bedzie dla dowolnej wywoływanej metody. | ||
Jesli jednak wywołamy funkcję | Jesli jednak wywołamy funkcję <tt>f1()</tt> za pomocą wyrażenia: | ||
(*pw).f1(); | (*pw).f1(); | ||
to ten mechanizm nie zadziała i nie ma możliwości, aby go w tej sytuacji | to ten mechanizm nie zadziała i nie ma możliwości, aby go w tej sytuacji | ||
Linia 496: | Linia 488: | ||
całego (lub prawie całego) zachowania klasy do innego obiektu, | całego (lub prawie całego) zachowania klasy do innego obiektu, | ||
nazywanego reprezentacją, a w obiekcie klasy przechowywanie tylko uchwytu | nazywanego reprezentacją, a w obiekcie klasy przechowywanie tylko uchwytu | ||
do reprezentacji (zob rysunek | do reprezentacji <font color=red>(zob rysunek 10.5 {[##fig:cow|Uzupelnic fig:cow|]})</font>. | ||
class Wichajster { | class Wichajster { | ||
public: | public: | ||
void do_something() {_rep->do_something();} | void do_something() {_rep->do_something();} | ||
private: | private: | ||
WichajsterRep _rep; | WichajsterRep _rep; | ||
} | } | ||
Techniki tej używamy np. kiedy chcemy oszczędzić czas i miejsce | Techniki tej używamy np. kiedy chcemy oszczędzić czas i miejsce | ||
potrzebne na kopiowanie obiektów. Kilka kopi obiektów klasy | potrzebne na kopiowanie obiektów. Kilka kopi obiektów klasy | ||
<tt>Wichajster</tt> może współdzielić jedną reprezentację korzystając np. | |||
ze zliczania referencji. Istotną różnicą w stosunku do inteligentnych | ze zliczania referencji. Istotną różnicą w stosunku do inteligentnych | ||
wskaźników jest zachowanie w przypadku zmiany jednego z obiektów. W | wskaźników jest zachowanie w przypadku zmiany jednego z obiektów. W | ||
Linia 524: | Linia 512: | ||
posiada on współdzieloną reprezentację, to tworzymy nową fizyczna | posiada on współdzieloną reprezentację, to tworzymy nową fizyczna | ||
kopię tej reprezentacji i dopiero ją zmieniamy. Przedstawione to jest | kopię tej reprezentacji i dopiero ją zmieniamy. Przedstawione to jest | ||
na rysunku | na <font color=red>rysunku 10.5 {[##fig:cow|Uzupelnic fig:cow|]}</font>. | ||
W przypadku metod łatwo stwierdzić które zmieniaja obiekt, a które nie, | W przypadku metod łatwo stwierdzić które zmieniaja obiekt, a które nie, | ||
Linia 531: | Linia 519: | ||
Cześciowym rozwiązaniem może być użycie obiektów proxy, tak jak to | Cześciowym rozwiązaniem może być użycie obiektów proxy, tak jak to | ||
opisano w poprzednim podrozdziale. Szczegółowy opis tej techniki | opisano w poprzednim podrozdziale. Szczegółowy opis tej techniki | ||
znajduje się w | znajduje się w <tt><i>"More Effective C++"</i></tt>. | ||
==10.4 Iteratory== | ==10.4 Iteratory== |
Wersja z 14:07, 23 sie 2006
10.1 Wstęp
Wskaźniki jeden z bardziej pożytecznych elementów języka C/C++, ale napewno najbardziej niebezpieczny. Zabawa z gołymi wskaźnikami przypomina żónglerkę odbezpieczonymi granatami. To nie jest już kwestia czy nastąpi wybuch, ale tego kiedy on nastąpi. Możliwości wywołania wybuchu i jego konsekwencje są wielorakie:
- Możemy usunąć wskaźnik nie zwalniając pamięci na którą on wskazuje. Jeśli żaden inny wskaźnik nie wskazuje na ten obszar to jest on bezpowrotnie stracony dla naszej aplikacji i mamy do czynienia z wyciekiem pamięci.
- Możemy zwolnić ten sam obszar pamięci kilkakrotnie. Powoduje to zwykle krach całej aplikacji.
- Podobnie jeśli spróbujemy zdereferencjonować wskaźniki null.
- No i oczywiście mamy całe spektrum możliwości sięgnięcia za pomocą wskaźnika tam gdzie nie powinniśmy, odczytując lub co gorsza zmianiając nie te zmiene co trzeba.
Jeśli więc nie czujemy się jak Rambo, albo nie przymierzając sam Chuck Norris, to powinniśmy poszukać jakiś zabezpieczeń. W C++ zabezpieczenia są dostarczane poprzez możliwość definicji własnych typów klas. Dzięki klasom możemy nie korzystać z dynamicznej alokacji pamięci bezpośrednio, ale za pośrednictwem klas, które dbają o alokacje w konstruktorach, dealokację w destruktorach, zwiększają i zmmniejszają pamięć na żądanie, itp. Przykładem takiego podejścia sa np. kontenery STL, jedną z zalet których jest właśnie zarządzanie własną pamięcią. Jeśli jednak ciągle potrzebujemy wskaźników to możemy rozważyć opakowanie ich w klasy. Jest to możliwe dzieki możliwości jakie w C++ daje przeładowywanie operatorów, w szczegolności możemy przeładowywać operatory dereferencjonowania operator*() i operator->(). W ten sposób możemy upodobnić zachowanie definiowanych przez nas typów do zachowania wskaźników. Takie typy nazywamy inteligentnymi wskaźnikami, ponieważ dostarczają nam dodatkowej funkcjonalności ponad zwykłe zachowanie wskaźnika.
Tak jak i u ludzi rodzaje inteligencji wskaźników bywają różne i inteligente wskaźniki występują w najróżniejszych wariacjach. Podział tych wariantów można przeprowadzić na wiele sposobów, ja skoncentruję sie na dwóch grupach:
- Zachowanie wskaźników podczas kopiowania, przypisywania i niszczenia. Nazwiemy to prawami własności.
- Zachowanie się operatorów operator*() i operator->(). Nazwiemy to kontrolą dostępu.
Poniżej krótko przedstawię przegląd głównych możliwości w każdej z powyższych grup.
10.2 Prawa własności
Głównym powodem używania inteligentnych wskaźników jest uzyskanie kontroli nad operacjami kopiowania, przypisywania i niszczenia wskaźnika. W tym kontekście mówimy często że wskaźnik jest albo nie jest właścicielem obiektu na który wskazuje. Poniżej przedstawiam cztery typowe schematy wskaźników.
10.2.1 Głupie wskaźniki
Zwykłe (nieinteligentne) wskaźniki, nie są właścicielami obiektów na ktorę wskazują. Kopiowanie czy przypisanie prowadzi do współdzielenia referencji (oba wskaźniki wskazują na ten sam obiekt) często niezamierzonej. Zniszczenie wskaźnika nie powoduje zniszczenia (dealokacji pamięci) obiektu na który on wskazuje. Przedstawia to rysunek 10.1 {[##fig:ptr_vulgaris|Uzupelnic fig:ptr_vulgaris|]} na którym zilustrowano przebieg wykonania kodu:
void f() { X *px( new X); X py(px); X pz(new X); pz=py; }
f();
[width=]{mod11/graphics/vulgaris.eps} Rysunek 10.1. Zwykłe wskaźniki.
10.2.2 Zliczanie referencji
Wskaźniki zliczające referencje są niejako właścicielami grupowymi obiektu na który wskazują. Kopiowanie i przypisanie powoduje współdzielnie referencji, ale kontrolowane, w tym sensie że monitorowana jest liczba wskaźników do danego obiektu. Na zasadzie "ostatni gasi światło" zniszczenie wskaźnika powoduje zniszczenie obiektu wtedy gdy był to jedyny (ostatni) wskaźnik na ten obiekt. Liczenie referencji reprezentuje więc prostą wersję "odśmieczacza" (garbage-collector). Zachowanie się tego typu wskaźników prezentuje rysunek 10.2 {[##fig:ptr_reference|Uzupelnic fig:ptr_reference|]} w oparciu o analogiczny kod.
void f() { ref_ptr<X> px(new X); ref_ptr<X> py(px); ref_ptr<X> pz(new X); pz=py; }
f();
[width=]{mod11/graphics/reference.eps} Rysunek 10.2. Wskaźniki zliczające referencje.
10.2.3 Głęboka/fizyczna kopia
Takie wskaźniki są pojedynczymi właścicielami obiektów na które wskazują i zachowują się jak wartości a nie wskaźniki. Kopiowanie bądź przypisanie powoduje fizyczne kopiowanie obiektu wskazywanego. Zniszczenie wskaźnika powoduje zniczenie wskazywanego obiektu. Od zwykłych wartości obiektów różnią się tym, że maja zachowanie polimorficzne i używane są tam gdzie polimorfizm jest nam potrzebny, a więc nie możemy użyć bezpośrednio samych obiektów. Zachowanie kodu
void f() { clone_ptr<X> px(new X); clone_ptr<X> py(px); cloen_ptr<X> pz(new X); pz=py; }
f();
ilustruje rysunek 10.3 {[##fig:deep_copy|Uzupelnic fig:deep_copy|]}.
[width=]{mod11/graphics/deep_copy.eps} Rysunek 10.3. Wskaźniki wykonujące kopie fizyczne.
Zastosowanie wskaźników z głębokim kopiowaniem zilustruję na przykładzie podanego już wcześniej przykładu z kształtami geometrycznymi. W programie wykorzystującym takie kształty na pewno zachodzi konieczność kopiowania kształtów. Załóżmy że wybraliśmy (myszką) jakiś kształt i wskaźnik do niego jest przechowywany w zmiennej Shape *selected. Załóżmy że jest to obiekt typy Circle. Teraz chcemy uzyskać kopie tego kształtu. Przypisanie
Shape *copy=selected;
Oczywiście nie zadziała bo uzyskamy dwa wskaźniki na jeden obiekt. A my potrzebujemy drugiego obiektu. Bez koniecznośći polimorfizmu wystarczyłoby użyć konstruktora kopiującego:
Shape *copy=new Shape(*selected);
Niestety w naszym przypadku ten kod się nawet nie skompiluje, bo klasa Shape jest klasą abstrakcyjną. Nawet gdyby nie była to i tak zostałby utworzony obiekt typu Shape a nie Circle. W celu zaimpleemntowania kopiowania polimorficznego możemy wyposażyć naszą klasę Shape w funkcje
virtual Shape *clone() const = 0
i następnie zdefiniować ją w każdej podklasie:
class Circle :public Shape { ... Circle *clone() {return new Circle(*this);}; }
i wtedy możemy skopiować (sklonować) nasz obiekt za pomocą
Shape *copy = selected->clone();
Możemy teraz tą technikę, nazywaną również wzorcem prototypu, lub fabryką klonów {gamma}, zastosować w implementacji inteligentnego wskaźnika clone_ptr
clone_ptr<Shape> selected; ... clone_ptr<Shape> copy(selected);
10.2.4 autoptr
Wskaźniki auto_ptr (jedyne inteligentne wskaźniki dostępne w standardzie C++) są pojedynczymi, bardzo zaborczymi, właścicielami obiektu na które wskazują. Tak zaborczymi że nie dopuszczają możliwości współdzielenia obiektu, ani jego kopiowania. Próba skopiowania, albo przypisania prowadzi do przekazania własności: Obiekt kopiowany(przypisywany) oddaje/przekazuje prawo własności do posiadanego obiektu, drugiemu obiektowi. Oznacza to że obiekt kopiowany lub przypisywany jest zmieniany w trakcie tych operacji. Ilustruje to rysunek 10.4 {[##fig:auto_ptr|Uzupelnic fig:auto_ptr|]} na podstawie kodu
void f() { auto_ptr<X> px(new X); auto_ptr<X> py(px); auto_ptr<X> pz(new X); pz=py; }
f();
[width=]{mod11/graphics/auto_ptr.eps} Rysunek 10.4. Wskaźniki autoptr.
To bardzo nieintuicyjne zachowanie: obiekty auto_ptr nie są modelami konceptu Assignable. Wskaźniki te zostały wprowadzone, aby wspomagać bezpieczną alokację zasobów (głownie pamięci), według wzorca "alokacja zasobów jest inicjalizacją" (Stroustrup). Rozważmy następujący przykład:
int f() { BigX *p = new BigX; ... tu coś się dzieje delete p; return wynik; }
Powyższy przykład to typowe zastosowanie dynamicznej alokacji pamięci. Problem polega na tym że jeżeli pomiędzy przydziałem pamięci jej zwolnieniem coś się stanie, to będziemy mieli wyciek pamięci. To coś to może być np. dodatkowe wyrażenie return lub rzucony wyjątek. W obu przypadkach zniszczone zostana wszystkie statycznie zaalokowane obikety w tym i wskaźnik p. Ale ponieważ jest to zwykły wskaźnik jego zniszczenie nie spowoduje zwolnienia wskazywanej przez niego pamięci. Rozwiązaniem jest właśnie uczynienie go obiektem będącym właścicielem wskazywanej pamięci:
int f() { auto_ptr<BigX> p(new BigX);
... tu coś się dzieje
return wynik; }
Teraz przy wyjściu z funkcji zostanie wywołany destruktor p, a on zwolni przydzieloną pamięć.
Wzkaźniki auto_ptr mogą być przekazywane i zwracane z funkcji. Jeśli przekażemy auto_ptr do funkcji przez wartość, to spowodowane tym kopiowanie spowoduje, że własność zostanie przekazana na argument funkcji i pamięć zostanie zwolniona kiedy funkcja zakończy swoje działanie.
template<typename T> void val(T p) { };
auto_ptr<X> px(new X); val(px); px zawiera wskaźnik null. pamięć jest zwolniona cout<<px.get()<<endl; zwraca opakowany wskaźnik na X, powinnien być zero
Jeśli przekażemy auto_ptr przez referencje to kopiowania nie będzie, przekazanie własności bedzie zależeć od tego czy wkaźnik zostanie skopiowany lub przypisany wewnątrz funkcji.
template<typename T> void ref_1(T &p) { T x = p; }; template<typename T> void ref_2(T &p) { };
auto_ptr<X> px(new X); ref_2(px); nic sie nie zmieniło cout<<px.get()<<endl; wypisuje jakiś adres ref_1(px) px zawiera wskaźnik null. pamięć jest zwolniona cout<<px.get()<<endl; zwraca opakowany wskaźnik na X, powinnien być zero
W przypadku przekazania auto_ptr jako referencji do stałej sprawa jest bardziej skomplikowana. Obecny standard stanowi, że wskaźnik auto_ptr przekazany jako referencja do stałej, nie przekazuje własności, tzn. operacje które by to tego prowadziły nie skompilują się. Z tych samych powodów nie powinien skompiluje się kod używający kontenerów STL zawierających wskaźniki auto_ptr.
template<typename T> void cref_1(const T &p) { T x = p; }; template<typename T> void cref_2(const T &p) { };
auto_ptr<X> px(new X); cref_2(px); OK, nic się nie stanie cout<<px.get()<<endl; wypisuje jakiś adres cref_1(px) nie skompiluje się
std::vector<auto_ptr<X> > v(10); nie skompiluje się
Różne implementacje różnie sobie z tym radzą, i w praktyce wynik kompilowania powyższych fragmentów kodu może być różny na różnych platformach. Jest to dość techniczne zagadnienie, zainteresowane osoby odsyłam do D. Vandervoorde, N. Josuttis: "C++ Szablony, Vademecum profesjonalisty" i STL.
10.3 Kontrola dostępu
Poza kontrolą rodzaju praw własności, inteligentny wskaźnik daje nam możliwość kontroli nad operacjami dostępu do wskazywanego obiektu poprzez operatory operator->() i operator*(). Wpływać na zachowanie tych operatorów możemy dwojako: po pierwsze w oczywisty sposób możemy wykonać dodatkowy kod zanim zwrócimy z nich odpowiednią wartość:
T &operator*() { zrob coś return *_p; }
Ten dodatkowy kod może np. sprawdzać czy wskaźnik _p nie jest zerowy, może zliczać wywołania, itp.
Po drugie możemy zmienić zwracany typ. Wbudowane operatory * i -> zwracają odpowiednio T& i T*. Oczywiście my możemy zwrócić cokolwiek, ale żeby to miało jakiś sens powinny to być obiekty zachowujące sie jak T& i T*. Takie obiekty które "zachowują się jak" coś, ale nie są tym (kwacze jak kaczka, ale to nie jest kaczka) nazywamy obiektami zastępczymi (proxy).
10.3.1 Proxy
Dlaczego moglibyśmy chcieć używać obiektów zastępczych?
Typowe zastosowanie to implemenatacja operacji przypisania do obiektów które tak naprawdę obiektami nie są. Weźmy jako przykład ostream_iterator dostarczany przez STL, który zezwala traktować plik wyjściowy jak kontener z iteratorem typu OutputIterator:
vector<int> V(10,7); copy(V.begin(), V.end(), ostream_iterator<int>(cout, ""));
Przypatrzny się temu przykładowi bliżej. Jeśli zdefiniujemy
ostream_iterator<int>(cout, "") iout;
to w zasadzie jedyną dozwoloną operacja jest przypisanie i zwiekszenie następujące po sobie:
(*iout) = 666; ++iout;
Ewidentnie nie istnieje żaden obiekt do którego referencje moglibyśmy zwrócić. Możemy jednak zwrócić obiekt zastępczy, który będzie definiował operator przypisania:
class writing_proxy { std::ostream &_out; public: writing_proxy(std::ostream &out) :_out(out) {};
void operator=(const T &val) { _out<<val; } };
Tę klasę zamkniemy wewnątrz klasy ostream_iterator
template<typename T> class ostream_iterator: public std::iterator <std::output_iterator_tag, T > {
class writing_proxy { ... };
std::string _sep; std::ostream &_out; writing_proxy _proxy; public: ostream_iterator(std::ostream &out,std::string sep): _out(out),_sep(sep),_proxy(_out){}; void operator++() {_out<<_sep;} void operator++(int) {_out<<_sep;} writing_proxy &operator*() {return _proxy;}; };
Dziedziczenie z klasy iterator zapewni nam, że nasz ostream_iterator posiada wszystkie typy stowarzyszone wymagane przez iteratory STL. To z kolei pociąga za soba możliwość użycia iterator_traits(zobacz {[##lbl:iterator-traits|Uzupelnic lbl:iterator-traits|]}). Bez tego nie moglibyśmy używać ostream_iterator w niektórych algorytmach STL.
Teraz możemy juz używać wyrażeń typu:
ostream_iterator<int> io(std::cout,""); (*io)=44;
Wywołanie *io zwraca writing_proxy. Następnie wywoływany jest
writing_proxy::operator=(44)}
który wykonuje operację
std::cout<<44;
Widać też że operacja
i=(*io)
się nie powiedzie (nie skompiluje). W tym przypadku jest to pożądane zachowanie,q bo taka operacja nie ma sensu. Jeśli byśmy jednak chcieli umożliwić działanie operacji przypisania w drugą stronę możemy w obiekcie proxy zdefiniować operator konwersji na typ T.
operator T() {return T();}; uwaga bzdurny przykład !!!
Wtedy wykonanie
i=(*io)
przypisze zero do zmiennej i. W ten sposób obiekty proxy pozwalają nam rozróżniać użycie operatora * do odczytu i do zapisu.
Obiekty zastępcze stanowią zresztą często spotykany wzorzec projektowy (zobacz STL). Poniżej przedstawię jeszcze jedną "sztuczkę" opisaną w Alexandrescu: "Nowoczesne Projektowanie" służącą do automatycznego obudowywania funkcji wywoływanych za pośrednictwem inteligentnego wskaźnika wywoływanami innych funkcji.
10.3.2 Opakowywanie wywołań funkcji
Załóżmy że mamy obiekt typu
struct Widget { void pre() {cout<<"pre"<<endl;}; void post() {cout<<"post"<<endl;};
void f1() {cout<<"f1"<<endl;} void f2() {cout<<"f2"<<endl;} }
Niech
Smart_prt<Widget> pw(new Widget);
bedzię inteligentnym wskaźnikiem do Widget. Chcemy aby każde wywołanie funkcji z Widget np.
pw->f1()
zostało poprzedzone poprzez wywołanie funkcji pre(), a po nim nastapiło wywołanie funkcji post(). Jedną z możliwości jest oczywiście zmiana kodu funkcji f?, tak aby wywoływały na początku pre() i post() na końcu. Można też dodać zestaw funkcji opakowywujących:
f1_wrapper() {pre();f1();post();}
Jest to jednak niepotrzebne duplikowanie kodu i możliwe do zastosowania tylko jeśli mamy możliwość zmiany kodu klasy Widget.
Można jednak zrobić inaczej. Zdefiniujemy pomocniczą klasę
template<typename T> struct Wrapper { T* _p; Wrapper(T* p):_p(p) {_p->pre();} ~Wrapper() {_p->post();}
T* operator->() {return _p;} };
W klasie inteligentnego wskaźnika przedefiniujemy operator->() tak, aby zwracał Wrapper<T>(T *) zamiastT*.
template<typename T> struct Smart_ptr { T *_p; Smart_ptr(T *p):_p(p) {}; ~Smart_ptr(){delete _p;};
Wrapper<T> operator->() {return Wrapper<T>(_p);}; T &operator*() {return *_p}; };
Jeśli teraz wywołamy
pw->f1();
to bedą się dziać następujace rzeczy:
- zostanie wywołany
pw.operator->();
operator ten zwraca obiekt tmp typu Wrapper<Widget> ale najpier musi go skonstruować, a więc
- zostanie wywołany konstruktor
tmp=Wrapper<Widget>(p);
który wywoła
p->pre();
- Jeśli operator->() zwróci obiekt który posiada operator->() to jest on wywoływany rekurencyjnie, aż zostanie zwrócony typ wskaźnikowy. Tak więc zostanie wywołany tmp.operator->() który zwroci p.
- Poprzez ten wskaźnik zostanie wywołana funkcja
p->f1();
- Zakończy się wywołanie pw.operator->() a więc zostanie wywołany destruktor obiektu tymczasowego tmp który wywoła
p->post();
Widać więc, że w końcu zostanie wykonana sekwencja wywołań:
p->pre(); p->f1(); p->post();
i tak bedzie dla dowolnej wywoływanej metody. Jesli jednak wywołamy funkcję f1() za pomocą wyrażenia:
(*pw).f1();
to ten mechanizm nie zadziała i nie ma możliwości, aby go w tej sytuacji zaimplementować. Może to być traktowane jako wada, bo nie jestśmy w stanie zapewnić, że każde wywołanie funkcji zostanie opakowane, ale z drugiej strony mamy do dyspozycji możliwość wyboru pomiędzy opakowanym i nieopakowanym wywołaniem funkcji.
10.3.3 Współdzielenie reprezentacji
Opisując inteligentne skaźniki nie można nie wspomnieć o technice implementacyjnej, która jest ściśle z nimi zwiazana, a mianowicie o współdzieleniu reprezentacji. Technika ta polega na oddelegowaniu całego (lub prawie całego) zachowania klasy do innego obiektu, nazywanego reprezentacją, a w obiekcie klasy przechowywanie tylko uchwytu do reprezentacji (zob rysunek 10.5 {[##fig:cow|Uzupelnic fig:cow|]}).
class Wichajster { public: void do_something() {_rep->do_something();} private: WichajsterRep _rep; }
Techniki tej używamy np. kiedy chcemy oszczędzić czas i miejsce potrzebne na kopiowanie obiektów. Kilka kopi obiektów klasy Wichajster może współdzielić jedną reprezentację korzystając np. ze zliczania referencji. Istotną różnicą w stosunku do inteligentnych wskaźników jest zachowanie w przypadku zmiany jednego z obiektów. W przypadku wskaźników, współdzielenie referencji jest planowaną cechą podejścia: kiedy zmieniamy obiekt poprzez jeden ze wskaźników wszystkie inne wskaźniki wskazują na zmieniony obiekt.
W przypadku współdzielenia reprezentacji chcemy cały czas rozróżniać obiekty, współdzielenie jest tylko technicznym środkiem optymalizacji. Wymaga to zastosowania techniki "copy on write", tzn. w momencie w którym dokonujemy na obiekcie operacji mogącej go zmienić i jeśli posiada on współdzieloną reprezentację, to tworzymy nową fizyczna kopię tej reprezentacji i dopiero ją zmieniamy. Przedstawione to jest na rysunku 10.5 {[##fig:cow|Uzupelnic fig:cow|]}.
W przypadku metod łatwo stwierdzić które zmieniaja obiekt, a które nie, problem jest tylko z metodami które zwracaja referencje do wnętrza obiektu. Takie metody mogą służyć zarówno do zapisy jak i do odczytu. Cześciowym rozwiązaniem może być użycie obiektów proxy, tak jak to opisano w poprzednim podrozdziale. Szczegółowy opis tej techniki znajduje się w "More Effective C++".
10.4 Iteratory
Iteratory to kolejny rodzaj inteligentnych wskaźników. Jeżli chodzi o prawa własności czy kontrolę dostępu to w większości zachowują się jak zwykłe wskaźniki. Wyjątkiem są specjalne iteratory takie jak {ostream_iterator}, czy {back_inserter}, wspomniane powyżej. Ale zasadniczo inteligencja iteratorów umiejscowiona jest w operacjach arytmetycznych. Chodzi głównie o operator {operator++()} ponieważ wyposażone są w niego wszystkie iteratory kontenerów z STL. To właśnie jest zresztą podstawowa rola iteratora: przechodzenie do kolejnych elementów, semantyka wskaźnika do już wybór twórcow STL.
10.5 Implementacje
Widać że różnorodność inteligentnych wskaźników może przyprawić o zawrót głowy. A nie rozważyliśmy jeszcze wszystkich kwestii dotyczących ich zachowania. Wyczerpująca dyskusja na ten temat znajduje się w {alexandrescu}. Tam też podana jest implemenatcja uniwersalnego szablony klasy inteligentnego wskaźnika parametryzowanego kilkoma klasami wytycznymi. Alternatywą jest użycie szeregu klas (szablonów) implementujacych jeden typ wskaźnika każda. Zbiór takich klas można znaleźć w bibliotece {boost} ({}). Bardzo dobre opisy implementacji inteligentnych wskaźników znajdują się również w {szablony} i {MeyersMECPP}.
Tutaj dla przykładu zaprezentuję implementację wskaźnika zliczającego referencję paramtryzowanego jedną klasą wytyczną. Jest to podejście zbliżone do {szablony}.
10.6 Zliczanie referencji
Implementacje zliczanie referencji różnią się przede wszystkim miejscem w którym umieszczony zostanie licznik. Dwie główne możliwości to wewnątrz lub na zewnątrz obiektu na który wskazujemy. Piersza możliwość jest ewidentnie możliwa tylko wtedy, jeśli mamy dostęp do kodu tej klasy. W każdej z tych dwu grup mamy dalsze możliwości np.
[width=]{mod11/graphics/counter.eps}
{Wkaźniki ze współdzileniem referencji.}
- Obiekt wskazywany udostępnia miejsce na licznik, zarządzaniem licznikiem
zajmuje się wskaźnik
- Obiekt wskazywany udostępnia nie tylko licznik, ale i interfejs do
zarządzania nim.
- Licznik jest osobnym obiektem. Każdy wskaźnik posiada wskaźnik
na obiekt wskazywany i wskaźnik na licznik (zobacz rysunek Uzupelnic fig:counter|).
- Licznik jest osobnym obiektem który zawiera również wskaźnik do
obiektu wskazywanego. Każdy wskaźnik zawiera tylko wskaźnik do licznika (zobacz rysunek Uzupelnic fig:counter|).
- Nie ma licznika, wskaźniki do tego samego obiektu połączone są w
listę (zobacz rysunek Uzupelnic fig:counter|).
Pokażę teraz przykladową implementację szablonu wskaźnika parametryzowanego jedną klasą wytyczną określającą którąś z powyższych strategii, aczkolwiek przy jednej wytycznej jest to wysiłek ktory pewnie sie nie opłaca, jako że kod wspólny jest dość mały. Ale implementacja ta może stanowić podstawe do rozszerzenia o kolejne wytyczne.
Najpierw musimy się zastanowić na interfejsem lub raczej konceptem klasy wytycznej. W sumie najłatwiej to zrobić implemetując konkretną wytyczną. Zaczniemy od osobnego zewnętrznego licznika (strategia Uzupelnic lbl:external|). Klasa wytyczna musi zawierać wsaźnik do wspólnego licznika:
template<typename T> struct Extra_counter_impl { /*...*/ private: size_t *_c; };
i funkcje zwiekszające i zmniejszające licznik:
public: bool remove_ref() {--(*_c);return *_c==0;}; void add_ref() {++(*_c);}; size_t count() {return *_c;}; };
Funkcja zmniejszająca licznik zwraca prawdę, jeśli usunięta została ostatnia referencja do wskazywanego obiektu. Potrzebna też będzie funkcja niszcząca licznik:
void cleanup() { delete _c; _c=0; }
Potrzebne będą dwa konstruktory: defaultowy, który nic nie robi:
Extra_counter_impl():_c(0) {};
i konstruktor inicjalizujący licznik obiektu powstającego po raz pierwszy:
Extra_counter_impl(T* p):_c(new size_t) {*_c=0;};
który przydziela pamięć dla licznika. Argument {T* p} służy tylko do rozróżnienia tych konstruktorów.
Korzystając z tej klasy nietrudno jest napsiać szablon inteligentnego wskaźnika. Obiekt licznika może być składować tego szablonu lub możemy dziedziczyć z klasy wytycznej (zob Uzupelnic |). Niestety okazę się że bedziemy mieli problem próbując zaimplementować inne strategie, w szczególności strategię w ktoręj licznik i wskaźnik na obiekt wskazywany znajdują się w tym samym obiekcie (strategia Uzupelnic lbl:indirect|). Dlatego zmienimy trochę naszą implementację wytycznej i założymy że obiekty tej klasy będą zawierać również wskaźnik na obiekt wskazywany.
T *_p;
i dodamy funkcję
T* pointee() {return _p;}
Musimy jeszcze poprawić funkcję czyszczącą
void cleanup() { delete _c; delete _p; _p=0; }
I jeden z konstruktorów:
Extra_counter_impl(T* p):_c(new size_t),_p(p) {*_c=0;};
Szablon wskaźnika korzystający z tak zdefiniowanej klasy wytycznej może wyglądać następująco:
template<typename T, typename counter_impl = Extra_counter_impl<T> > class Ref_ptr { public: Ref_ptr() {}; Ref_ptr(T *p):_c(p) { _c.add_ref(); };
Ref_ptr() {detach();}
Ref_ptr(const Ref_ptr &p):_c(p._c) { _c.add_ref(); }
Ref_ptr &operator=(const Ref_ptr &rhs) { if(this!=&rhs) { detach(); _c=rhs._c; _c.add_ref(); } return *this; }
T* operator->() {return _c.pointee();} T &operator*() {return *(_c.pointee());}
size_t count() {return _c.count();}; private: mutable counter_impl _c; void detach() { if (_c.remove_ref() ) _c.cleanup(); }; };
10.7 autoptr
Implementacja wskaźnika {auto_ptr} oparta jest o dwie funkcje. Jedna zwalnia przechowywany wskaźnik zwracając go na zewnątrz i oddając własność:
T* release() { T *oldPointee = pointee; pointee = 0; return oldPointee; }
{pointee} jest przechowywanym (zwykłym) wskaźnikiem.
private: T *pointee;
Druga funkcja zamienia przechowywany wskaźnik na inny, zwolaniając wskazywaną przez niego pamięć:
void reset(T *p = 0) { if (pointee != p) { delete pointee; pointee = p; } }
Za pomocą tych funkcji można już łatwo zimplementować resztę szablonu np.:
template<class T> class auto_ptr { public: explicit auto_ptr(T *p = 0): pointee(p) {}
template<class U> auto_ptr(auto_ptr& rhs): pointee(rhs.release()) {}
auto_ptr() { delete pointee; }
template<class U> auto_ptr<T>& operator=(auto_ptr& rhs) { if (this != &rhs) reset(rhs.release()); return *this; }
T& operator*() const { return *pointee; } T* operator->() const { return pointee; } T* get() const { return pointee; } }
Konstruktor kopiujacy i operator przypisania są szablonami, w ten sposób można kopiować również wskaźniki {auto_ptr} opakowywujące typy które mogą być na siebie rzutowane np. można przypisać {auto_ptr<Derived>} do {auto_ptr<Base>}, jeśli {Derived} dziedziczy publicznie z {Base}. Konstruktor {auto_ptr(T *p = 0)} został zadeklarowany jako {explicit} wobec czego nie spowoduje niejawnej konwresji z typu {T*} na {auto_ptr<T>}.
Różne impelentacje {auto_ptr} różnią się szczegułami dotyczącymi obsługi {const auto_ptr} i przekazywania {auto_ptr} przez stałą referencje. Powyższa implentacja wzięta z {MeyersMECPP}, nie posiada pod tym względem żadnych zabezpieczeń. Szczegółowa dyskusja tego zagadnienia i bardziej zaawansowana implementacja znajduje się w {josuttis}. Temat ten jest też poruszony w {szablony}. Warto też zaglądnąć do implementacji {auto_ptr} dostarczonej z kompilatorem {g++}.