|
|
Linia 4: |
Linia 4: |
| {Inteligentne wskaźniki} | | {Inteligentne wskaźniki} |
|
| |
|
| ==Wstęp==
| | '''Zadanie 1 ''' Napisz testy sprawdzające działanie szablony |
| | inteligentnego wskaźnika opartego na zliczaniu referencji. |
|
| |
|
| Wskaźniki to jeden z bardziej pożytecznych elementów języka C/C++, ale
| | '''Zadanie 2 ''' Napisz klasę wytyczna do wskaźnika <code><nowiki> Ref_ptr</nowiki></code> opartą o |
| napewno najbardziej niebezpieczny. Zabawa z gołymi wskaźnikami
| | listę referencji. |
| przypomina żonglerkę 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
| |
| zerowe (<code><nowiki> null</nowiki></code>).
| |
| * 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,
| |
| zmieniając, nie te zmienne co trzeba.
| |
|
| |
| Jeśli więc nie czujemy się jak Rambo albo sam Chuck
| |
| Norris, to powinniśmy poszukać jakiś zabezpieczeń. W C++
| |
| zabezpieczenia są dostarczane poprzez możliwość definicji własnych
| |
| typów. Dzięki klasom możemy nie korzystać z dynamicznej alokacji
| |
| pamięci bezpośrednio, ale za pośrednictwem klas, które dbają o alokację
| |
| w konstruktorach, dealokację w destruktorach, zwiększają i
| |
| zmmniejszają pamięć na żądanie, itp. Przykładem takiego podejścia są
| |
| np. kontenery STL, których jedną z zalet, 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 dzięki możliwościom,
| |
| jakie w C++ daje przeładowywanie operatorów. W szczególności możemy
| |
| przeładowywać operatory dereferencjonowania <code><nowiki> operator*()</nowiki></code> i
| |
| <code><nowiki> operator->()</nowiki></code>. 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;
| |
| inteligente wskaźniki występują w najróżniejszych wariacjach. Podział
| |
| tych wariantów można przeprowadzić na wiele sposobów, ja skoncentruję
| |
| się na dwóch grupach:
| |
| * Zachowanie wskaźników podczas kopiowania, przypisywania
| |
| i niszczenia. Nazwiemy to prawami własności.
| |
| * Zachowanie się operatorów <code><nowiki> operator*()</nowiki></code> i <code><nowiki> operator->()</nowiki></code>.
| |
| Nazwiemy to kontrolą dostępu.
| |
|
| |
| Poniżej krótko przedstawię przegląd głównych możliwości w każdej z
| |
| powyższych grup.
| |
|
| |
|
| ==Prawa własności==
| | '''Zadanie 3 ''' Napisz klasę wytyczną do wskaźnika <code><nowiki> ref_ptr</nowiki></code> opartą o licznik we wskazywanym obiekcie. Załóż, że obiekty wskazywane dziedziczą z klasy: |
|
| |
|
| Głównym powodem używania inteligentnych wskaźników jest uzyskanie
| | <nowiki> class Handle { |
| kontroli nad operacjami kopiowania, przypisywania i niszczenia
| | private: |
| wskaźnika. W tym kontekście mówimy często, że wskaźnik jest albo nie
| | size_t _count; |
| jest właścicielem obiektu, na który wskazuje. Poniżej przedstawiam cztery
| |
| typowe schematy wskaźników.
| |
| | |
| ====Głupie wskaźniki====
| |
| | |
| Zwykłe (nieinteligentne) wskaźniki nie są właścicielami obiektów, na
| |
| którę 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 [[##fig:ptr_vulgaris|Uzupelnic fig:ptr_vulgaris|]] na którym zilustrowano
| |
| przebieg wykonania kodu:
| |
| | |
| <nowiki> void f() {
| |
| X *px( new X);
| |
| X py(px);
| |
| X pz(new X);
| |
| pz = py;
| |
| }
| |
| | |
| f();
| |
| </nowiki>
| |
| | |
| [width<nowiki> =</nowiki> ]{mod11/graphics/vulgaris.eps}
| |
|
| |
| {Zwykłe wskaźniki.}
| |
|
| |
| ====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 [[##fig:ptr_reference|Uzupelnic fig:ptr_reference|]] w oparciu o analogiczny kod:
| |
| | |
| <nowiki> void f() {
| |
| ref_ptr<X> px(new X);
| |
| ref_ptr<X> py(px);
| |
| ref_ptr<X> pz(new X);
| |
| pz = py;
| |
| }
| |
| | |
| f();
| |
| </nowiki>
| |
| | |
| [width<nowiki> =</nowiki> ]{mod11/graphics/reference.eps}
| |
|
| |
| {Wskaźniki zliczające referencje.}
| |
|
| |
| ====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 mają 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:
| |
| | |
| <nowiki> void f() {
| |
| clone_ptr<X> px(new X);
| |
| clone_ptr<X> py(px);
| |
| cloen_ptr<X> pz(new X);
| |
| pz = py;
| |
| }
| |
| | |
| f();
| |
| </nowiki>
| |
| | |
| ilustruje rysunek [[##fig:deep_copy|Uzupelnic fig:deep_copy|]].
| |
|
| |
| [width<nowiki> =</nowiki> ]{mod11/graphics/deep_copy.eps}
| |
|
| |
| {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 <code><nowiki> Shape *selected</nowiki></code>. Załóżmy, że jest to obiekt typy
| |
| <code><nowiki> Circle</nowiki></code>. Teraz chcemy uzyskać kopię tego kształtu. Przypisanie
| |
| | |
| <nowiki> Shape *copy = selected;
| |
| </nowiki>
| |
| | |
| oczywiście nie jest poprawne, bo uzyskamy dwa wskaźniki na jeden obiekt. A
| |
| my potrzebujemy drugiego obiektu. Bez koniecznośći polimorfizmu
| |
| wystarczyłoby użyć konstruktora kopiującego:
| |
| | |
| <nowiki> Shape *copy = new Shape(*selected);
| |
| </nowiki>
| |
| | |
| Niestety w naszym przypadku ten kod się nawet nie skompiluje, bo klasa
| |
| <code><nowiki> Shape</nowiki></code> jest klasą abstrakcyjną. Nawet gdyby nie była, to i tak
| |
| zostałby utworzony obiekt typu <code><nowiki> Shape</nowiki></code> a nie <code><nowiki> Circle</nowiki></code>. W celu
| |
| zaimplementowania kopiowania polimorficznego możemy wyposażyć naszą
| |
| klasę <code><nowiki> Shape</nowiki></code> w funkcje
| |
| | |
| <nowiki> virtual Shape *clone() const = 0
| |
| </nowiki>
| |
| | |
| i następnie zdefiniować ją w każdej podklasie:
| |
| | |
| <nowiki> class Circle :public Shape { | |
| ...
| |
| Circle *clone() {return new Circle(*this);};
| |
| }
| |
| </nowiki>
| |
| | |
| Możemy wtedy skopiować (sklonować) nasz obiekt za pomocą:
| |
| | |
| <nowiki> Shape *copy = selected->clone();
| |
| </nowiki>
| |
| | |
| Możemy teraz tę technikę, nazywaną również wzorcem prototypu, lub
| |
| fabryką klonów (zob. {gamma}), zastosować w implementacji inteligentnego
| |
| wskaźnika <code><nowiki> clone_ptr</nowiki></code>:
| |
| | |
| <nowiki> clone_ptr<Shape> selected;
| |
| ...
| |
| clone_ptr<Shape> copy(selected);
| |
| </nowiki>
| |
| | |
| ====autoptr====
| |
| | |
| Wskaźniki <code><nowiki> auto_ptr</nowiki></code> (jedyne inteligentne wskaźniki dostępne w
| |
| standardzie C++) są pojedynczymi, bardzo zaborczymi, właścicielami obiektu,
| |
| na który 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 [[##fig:auto_ptr|Uzupelnic fig:auto_ptr|]] na podstawie kodu:
| |
| | |
| <nowiki> void f() {
| |
| auto_ptr<X> px(new X);
| |
| auto_ptr<X> py(px);
| |
| auto_ptr<X> pz(new X);
| |
| pz = py;
| |
| }
| |
| | |
| f();
| |
| </nowiki>
| |
| | |
| [width<nowiki> =</nowiki> ]{mod11/graphics/auto_ptr.eps}
| |
|
| |
| {Wskaźniki <tt>autoptr</tt>.}
| |
|
| |
| To bardzo nieintuicyjne zachowanie:
| |
| obiekty <code><nowiki> auto_ptr</nowiki></code> nie są modelami konceptu <code><nowiki> Assignable</nowiki></code>.
| |
| Wskaźniki te zostały wprowadzone, aby wspomagać bezpieczną alokację
| |
| zasobów (głównie pamięci), według wzorca "alokacja zasobów jest
| |
| inicjalizacją" {Stroustrup}. Rozważmy następujący przykład:
| |
| | |
| <nowiki> int f() {
| |
| BigX *p = new BigX;
| |
| /*... tu coś się dzieje */
| |
| delete p;
| |
| return wynik;
| |
| }
| |
| </nowiki>
| |
| | |
| To typowe zastosowanie dynamicznej alokacji pamięci.
| |
| Problem polega na tym, że jeżeli pomiędzy przydziałem pamięci a jej
| |
| zwolnieniem coś się stanie, to będziemy mieli wyciek pamięci. To coś,
| |
| to może być np. dodatkowe wyrażenie <code><nowiki> return</nowiki></code> lub rzucony wyjątek.
| |
| W obu przypadkach zniszczone zostaną wszystkie statycznie zaalokowane
| |
| obiekty w tym i wskaźnik <code><nowiki> p</nowiki></code>. 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:
| |
| | |
| <nowiki> int f() {
| |
| auto_ptr<BigX> p(new BigX);
| |
| | |
| /*... tu coś się dzieje */
| |
| | |
| return wynik;
| |
| }
| |
| </nowiki>
| |
| | |
| Teraz przy wyjściu z funkcji zostanie wywołany destruktor <code><nowiki> p</nowiki></code>, a on
| |
| zwolni przydzieloną pamięć.
| |
| | |
| Wzkaźniki <code><nowiki> auto_ptr</nowiki></code> mogą być przekazywane i zwracane z funkcji.
| |
| Jeśli przekażemy <code><nowiki> auto_ptr</nowiki></code> 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.
| |
| | |
| <nowiki> 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*/
| |
| </nowiki>
| |
| | |
| Jeśli przekażemy <code><nowiki> auto_ptr</nowiki></code> przez referencje, to kopiowania nie
| |
| będzie, przekazanie własności będzie zależeć od tego, czy wkaźnik
| |
| zostanie skopiowany lub przypisany wewnątrz funkcji.
| |
| | |
| <nowiki> 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*/
| |
| </nowiki>
| |
| | |
| W przypadku przekazania <code><nowiki> auto_ptr</nowiki></code> jako referencji do stałej sprawa
| |
| jest bardziej skomplikowana. Obecny standard stanowi, że wskaźnik
| |
| <code><nowiki> auto_ptr</nowiki></code> przekazany jako referencja do stałej nie przekazuje
| |
| własności, tzn. operacje, które by to tego prowadziły nie powinny się
| |
| skompilować. Z tych samych powodów nie powinien skompilować się kod
| |
| używający kontenerów STL zawierających wskaźniki <code><nowiki> auto_ptr</nowiki></code>.
| |
| | |
| <nowiki> 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ę*/
| |
| /* {mod11/code/auto_ptr.cpp}{autoptr.cpp}*/
| |
| </nowiki>
| |
| | |
| 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 {szablony} i {josuttis}.
| |
| | |
| ==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 <code><nowiki> operator->()</nowiki></code> i <code><nowiki> operator*()</nowiki></code>. 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ść:
| |
| | |
| <nowiki> T &operator*() {
| |
| /* zrob coś */
| |
| return *_p;
| |
| }
| |
| </nowiki>
| |
| | |
| Ten dodatkowy kod może np. sprawdzać, czy wskaźnik <code><nowiki> _p</nowiki></code> nie jest
| |
| zerowy, może zliczać wywołania, itp.
| |
| | |
| Po drugie, możemy zmienić zwracany typ. Wbudowane operatory <code><nowiki> *</nowiki></code> i
| |
| <code><nowiki> -></nowiki></code> zwracają odpowiednio <code><nowiki> T&</nowiki></code> i <code><nowiki> T*</nowiki></code>. Oczywiście my możemy
| |
| zwrócić cokolwiek, ale żeby to miało jakiś sens, powinny to być obiekty
| |
| zachowujące się jak <code><nowiki> T&</nowiki></code> i <code><nowiki> T*</nowiki></code>. Takie obiekty, które "zachowują
| |
| się jak coś", ale tym nie są (kwacze jak kaczka, ale to nie jest
| |
| kaczka) nazywamy obiektami zastępczymi (proxy).
| |
| | |
| ===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
| |
| <code><nowiki> ostream_iterator</nowiki></code> dostarczany przez STL, który zezwala traktować
| |
| plik wyjściowy jak kontener z iteratorem typu <code><nowiki> OutputIterator</nowiki></code>:
| |
| | |
| <nowiki> vector<int> V(10,7);
| |
| copy(V.begin(), V.end(), ostream_iterator<int>(cout, ""));
| |
| </nowiki>
| |
| | |
| Przypatrzny się temu przykładowi bliżej. Jeśli zdefiniujemy
| |
| | |
| <nowiki> ostream_iterator<int>(cout, "") iout;
| |
| </nowiki>
| |
| | |
| to w zasadzie jedyną dozwoloną operacją jest przypisanie i zwiększenie
| |
| następujące po sobie:
| |
| | |
| <nowiki> (*iout) = 666; ++iout;
| |
| </nowiki>
| |
| | |
| 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:
| |
| | |
| <nowiki> class writing_proxy {
| |
| std::ostream &_out;
| |
| public:
| |
| writing_proxy(std::ostream &out) :_out(out) {};
| |
|
| |
| void operator = (const T &val) {
| |
| _out<<val;
| |
| }
| |
| };
| |
| /* {mod11/code/out.cpp}{out.cpp} */
| |
| </nowiki>
| |
| | |
| Tę klasę zamkniemy wewnątrz klasy <code><nowiki> ostream_iterator</nowiki></code>
| |
| | |
| <nowiki> 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: | | public: |
| ostream_iterator(std::ostream &out,std::string sep): | | Handle():_count(0){}; |
| _out(out),_sep(sep),_proxy(_out){};
| |
| void operator++() {_out<<_sep;}
| |
| void operator++(int) {_out<<_sep;}
| |
| writing_proxy &operator*() {return _proxy;};
| |
| };
| |
| /* {mod11/code/out.cpp}{out.cpp} */
| |
| </nowiki>
| |
|
| |
|
| Dziedziczenie z klasy <code><nowiki> iterator</nowiki></code> zapewni nam, że nasz
| | void add_ref() { ++_count;} |
| <code><nowiki> ostream_iterator</nowiki></code> posiada wszystkie typy stowarzyszone wymagane
| | bool remove_ref() {--_count; return _count = = 0;} |
| przez iteratory STL. To z kolei pociąga za sobą możliwość użycia
| | size_t count() const {return _count;}; |
| <code><nowiki> iterator_traits</nowiki></code>(zobacz [[##lbl:iterator-traits|Uzupelnic lbl:iterator-traits|]]). Bez tego nie
| |
| moglibyśmy używać <code><nowiki> ostream_iterator</nowiki></code> w niektórych algorytmach STL.
| |
| | |
| Teraz możemy juz używać wyrażeń typu:
| |
| | |
| <nowiki> ostream_iterator<int> io(std::cout,"");
| |
| (*io) = 44;
| |
| /* {mod11/code/out.cpp}{out.cpp} */
| |
| </nowiki>
| |
| | |
| Wywołanie <code><nowiki> *io</nowiki></code> zwraca <code><nowiki> writing_proxy</nowiki></code>. Następnie wywoływany
| |
| jest
| |
| | |
| <nowiki> writing_proxy::operator = (44)}
| |
| </nowiki>
| |
| | |
| który wykonuje operację
| |
| | |
| <nowiki> std::cout<<44;
| |
| </nowiki>
| |
| | |
| Widać też, że operacja
| |
| | |
| <nowiki> i = (*io)
| |
| </nowiki>
| |
| | |
| nie powiedzie się (nie skompiluje). W tym przypadku jest to pożądane
| |
| zachowanie, bo taka operacja nie ma sensu. Jeślibyśmy jednak
| |
| chcieli umożliwić działanie operacji przypisania w drugą stronę możemy
| |
| w obiekcie proxy zdefiniować operator konwersji na typ <code><nowiki> T</nowiki></code>.
| |
| | |
| <nowiki> operator T() {return T();}; /* uwaga bzdurny przykład !!! */
| |
| </nowiki>
| |
| | |
| Wtedy wykonanie
| |
| | |
| <nowiki> i = (*io)
| |
| </nowiki>
| |
| | |
| przypisze zero do zmiennej <code><nowiki> i</nowiki></code>. W ten sposób obiekty proxy
| |
| pozwalają nam rozróżniać użycie operatora <code><nowiki> *</nowiki></code> do odczytu i do
| |
| zapisu.
| |
| | |
| Obiekty zastępcze stanowią zresztą często spotykany wzorzec projektowy
| |
| (zobacz {gamma}). Poniżej przedstawię jeszcze jedną "sztuczkę"
| |
| opisaną w {alexandrescu}.
| |
| | |
| ===Opakowywanie wywołań funkcji===
| |
| | |
| Załóżmy, że mamy obiekt typu:
| |
| | |
| <nowiki> struct Widget {
| |
| void pre() {cout<<"pre"<<endl;}; | |
| void post() {cout<<"post"<<endl;}; | |
| | |
| void f1() {cout<<"f1"<<endl;}
| |
| void f2() {cout<<"f2"<<endl;}
| |
| /*{mode11/code/pre_post.cpp}{prepost.cpp} */
| |
| }
| |
| </nowiki>
| |
| | |
| Niech
| |
| | |
| <nowiki> Smart_prt<Widget> pw(new Widget);
| |
| </nowiki>
| |
| | |
| będzię inteligentnym wskaźnikiem do <code><nowiki> Widget</nowiki></code>.
| |
| Chcemy, aby każde wywołanie funkcji z <code><nowiki> Widget</nowiki></code> np.
| |
| | |
| <nowiki> pw->f1()
| |
| </nowiki>
| |
| | |
| zostało poprzedzone poprzez wywołanie funkcji <code><nowiki> pre()</nowiki></code>, a po nim
| |
| nastąpiło wywołanie funkcji <code><nowiki> post()</nowiki></code>. Jedną z możliwości jest
| |
| oczywiście zmiana kodu funkcji <code><nowiki> f1</nowiki></code> i <code><nowiki> f2</nowiki></code>,
| |
| tak aby wywoływały na początku
| |
| <code><nowiki> pre()</nowiki></code> i <code><nowiki> post()</nowiki></code> na końcu. Można też dodać zestaw funkcji
| |
| opakowywujących:
| |
| | |
| <nowiki> f1_wrapper() {pre();f1();post();}
| |
| </nowiki>
| |
| | |
| Jest to jednak niepotrzebne duplikowanie kodu i możliwe do
| |
| zastosowania tylko jeśli mamy możliwość zmiany kodu klasy <code><nowiki> Widget</nowiki></code>.
| |
| | |
| Można jednak zrobić inaczej. Zdefiniujemy pomocniczą klasę:
| |
| | |
| <nowiki> template<typename T> struct Wrapper {
| |
| T* _p;
| |
| Wrapper(T* p):_p(p) {_p->pre();}
| |
| Wrapper() {_p->post();} | |
| | |
| T* operator->() {return _p;} | |
| }; | | }; |
| /*{mode11/code/pre_post.cpp}{prepost.cpp} */
| |
| </nowiki>
| |
|
| |
| W klasie inteligentnego wskaźnika przedefiniujemy <code><nowiki> operator->()</nowiki></code>
| |
| tak, aby zwracał <code><nowiki> Wrapper<T>(T *)</nowiki></code> zamiast<code><nowiki> T*</nowiki></code>.
| |
|
| |
| <nowiki> 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};
| |
| /*{mode11/code/pre_post.cpp}{prepost.cpp} */
| |
| };
| |
| </nowiki>
| |
|
| |
| Jeśli teraz wywołamy
| |
|
| |
| <nowiki> pw->f1();
| |
| </nowiki>
| |
|
| |
| to będą się dziać następujace rzeczy:
| |
| * zostanie wywołany
| |
|
| |
| <nowiki> pw.operator->();
| |
| </nowiki>
| |
|
| |
| operator ten zwraca obiekt <code><nowiki> tmp</nowiki></code> typu <code><nowiki> Wrapper<Widget></nowiki></code>, ale
| |
| najpier musi go skonstruować, a więc
| |
| * zostanie wywołany konstruktor
| |
|
| |
| <nowiki> tmp = Wrapper<Widget>(p);
| |
| </nowiki>
| |
|
| |
| który wywoła
| |
|
| |
| <nowiki> p->pre();
| |
| </nowiki>
| |
| * Jeśli <code><nowiki> operator->()</nowiki></code>, zwróca obiekt który posiada
| |
| <code><nowiki> operator->()</nowiki></code>, to jest on wywoływany rekurencyjnie, aż zostanie
| |
| zwrócony typ wskaźnikowy. Tak więc zostanie wywołany
| |
| <code><nowiki> tmp.operator->()</nowiki></code> który zwróci <code><nowiki> p</nowiki></code>.
| |
| * Poprzez ten wskaźnik zostanie wywołana funkcja
| |
|
| |
| <nowiki> p->f1();
| |
| </nowiki>
| |
| * Zakończy się wywołanie <code><nowiki> pw.operator->()</nowiki></code>, a więc zostanie
| |
| wywołany destruktor obiektu tymczasowego <code><nowiki> tmp</nowiki></code> który wywoła
| |
|
| |
| <nowiki> p->post();
| |
| </nowiki>
| |
|
| |
| Widać więc, że w końcu zostanie wykonana sekwencja wywołań:
| |
|
| |
| <nowiki> p->pre();
| |
| p->f1();
| |
| p->post();
| |
| </nowiki> | | </nowiki> |
|
| |
|
| i tak będzie dla dowolnej wywoływanej metody.
| | '''Zadanie 4 ''' |
| Jesli jednak wywołamy funkcję <code><nowiki> f1()</nowiki></code> za pomocą wyrażenia:
| | Zaimplementuj iterator pozwalający wkładać wartości na koniec |
| | | kontenera. |
| <nowiki> (*pw).f1();
| |
| </nowiki>
| |
| | |
| 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 jesteś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.
| |
| | |
| ==Współdzielenie reprezentacji==
| |
| | |
| Opisując inteligentne skaźniki, nie można nie wspomnieć o technice
| |
| implementacyjnej, która jest ściśle z nimi związana, 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 [[##fig:cow|Uzupelnic fig:cow|]]).
| |
| [p]
| |
|
| |
| {}
| |
|
| |
| <nowiki> class Wichajster {
| |
| public:
| |
| void do_something() {_rep->do_something();}
| |
| private:
| |
| WichajsterRep _rep;
| |
| }
| |
| </nowiki>
| |
| | |
| Techniki tej używamy np. kiedy chcemy oszczędzić czas i miejsce
| |
| potrzebne na kopiowanie obiektów. Kilka kopi obiektów klasy
| |
| <code><nowiki> Wichajster</nowiki></code> 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 [[##fig:cow|Uzupelnic fig:cow|]].
| |
| | |
| W przypadku metod łatwo stwierdzić, które zmieniaja obiekt, a które nie;
| |
| problem występuje tylko z metodami które zwracaja referencje do wnętrza
| |
| obiektu. Takie metody mogą służyć zarówno do zapisu 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 {MeyersMECPP}.
| |
| | |
| ==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
| |
| <code><nowiki> ostream_iterator</nowiki></code>, czy <code><nowiki> back_inserter</nowiki></code>, wspomniane powyżej.
| |
| Ale zasadniczo inteligencja iteratorów umiejscowiona jest w operacjach
| |
| arytmetycznych. Chodzi głównie o operator <code><nowiki> operator++()</nowiki></code>, 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 to już wybór twórców STL.
| |
| | |
| ==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 szablonu 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 <code><nowiki> boost</nowiki></code>
| |
| ({<math>\displaystyle BOOST_LIBS/smart_ptr/smart_ptr.htm}{smart\_ptr}).
| |
| Bardzo dobre opisy implementacji inteligentnych wskaźników znajdują
| |
| się również w \cite{szablony} i \cite{MeyersMECPP}.
| |
| | |
| Tutaj dla przykładu zaprezentuję implementację wskaźnika zliczającego
| |
| referencję, parametryzowanego jedną klasą wytyczną. Jest to podejście
| |
| zbliżone do \cite{szablony}.
| |
| | |
| ==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. Pierwszy
| |
| wariant jest ewidentnie możliwy tylko wtedy, jeśli mamy dostęp do
| |
| kodu tej klasy. W każdej z tych dwu grup mamy dalsze możliwości np:
| |
| \beginfigure
| |
| \begincenter
| |
| \includegraphics[width=\floatwidth]{mod11/graphics/counter.eps}
| |
| \endcenter
| |
| \caption{Wkaźniki ze współdzielniem referencji.}
| |
| \endfigure
| |
| # 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~[[##fig:counter|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~[[##fig:counter|Uzupelnic fig:counter|]]).
| |
| # Nie ma licznika, wskaźniki do tego samego obiektu połączone są w
| |
| listę (zobacz rysunek~[[##fig:counter|Uzupelnic fig:counter|]]).
| |
| | | |
| Pokażę teraz przykładową implementację szablonu wskaźnika
| | '''Zadanie 5 ''' Zaimplementuj iterator realizujący przechodzenie po drzewie binarnym. Drzwo opearte jest o strukturę: |
| parametryzowanego jedną klasą wytyczną określającą którąś z powyższych
| |
| strategii, aczkolwiek przy jednej wytycznej jest to wysiłek który
| |
| pewnie się nie opłaca, jako że kod wspólny jest dość mały. Ale
| |
| implementacja ta może stanowić podstawę 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~[[##lbl:external|Uzupelnic lbl:external|]]).
| |
| Klasa wytyczna musi zawierać wsaźnik do wspólnego licznika:
| |
| | |
| <nowiki> template<typename T> struct Extra_counter_impl {
| |
| /*...*/
| |
| private:
| |
| size_t *_c;
| |
| };
| |
| </nowiki>
| |
| | |
| i funkcje zwiększające i zmniejszające licznik:
| |
| | |
| <nowiki> public:
| |
| bool remove_ref() {--(*_c);return *_c==0;};
| |
| void add_ref() {++(*_c);};
| |
| size_t count() {return *_c;};
| |
| };
| |
| </nowiki>
| |
| | |
| 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:
| |
| | |
| <nowiki> void cleanup() { | |
| delete _c;
| |
| _c=0;
| |
| }
| |
| </nowiki>
| |
| | |
| Potrzebne będą dwa konstruktory: defaultowy, który nic nie robi:
| |
| | |
| <nowiki> Extra_counter_impl():_c(0) {}; | |
| </nowiki>
| |
| | |
| i konstruktor inicjalizujący licznik obiektu powstającego po raz pierwszy:
| |
| | |
| <nowiki> Extra_counter_impl(T* p):_c(new size_t) {*_c=0;};
| |
| </nowiki>
| |
| | |
| który przydziela pamięć dla licznika. Argument <code><nowiki> T* p</nowiki></code> służy tylko
| |
| do rozróżnienia tych konstruktorów.
| |
| | |
| Korzystając z tej klasy nietrudno jest, napisać szablon inteligentnego
| |
| wskaźnika. Obiekt klasy <code><nowiki> Extra_counter_impl</nowiki></code> może być składową
| |
| tego szablonu lub możemy z tej klasy wytycznej
| |
| dziedziczyć (zob~[[##lbl:wytyczne|Uzupelnic lbl:wytyczne|]]).
| |
| | |
| Niestety, przy tak zdefiniowanym interfejsie do klasy wytycznej,
| |
| bedziemy mieli problem próbując zaimplementować inne strategie, w
| |
| szczególności strategię w której licznik i wskaźnik na obiekt
| |
| wskazywany znajdują się w tym samym obiekcie
| |
| (strategia~[[##lbl:indirect|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.
| |
| | |
| <nowiki> T *_p;
| |
| </nowiki>
| |
| | |
| i dodamy funkcję
| |
| | |
| <nowiki> T* pointee() {return _p;}
| |
| </nowiki>
| |
| | |
| Musimy jeszcze poprawić funkcję czyszczącą:
| |
| | |
| <nowiki> void cleanup() {
| |
| delete _c;
| |
| delete _p;
| |
| _p=0;
| |
| }
| |
| /* \href{mod11/code/ref_ptr.h}{ref\_ptr.h} */
| |
| </nowiki>
| |
| | |
| I jeden z konstruktorów:
| |
| | |
| <nowiki> Extra_counter_impl(T* p):_c(new size_t),_p(p) {*_c=0;};
| |
| </nowiki>
| |
| | |
| Szablon wskaźnika korzystający z tak zdefiniowanej klasy wytycznej może
| |
| wyglądać następująco:
| |
| | |
| <nowiki> 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) {
| | <nowiki> template<typename T> class binary_tree { |
| _c.add_ref();
| |
| }
| |
|
| |
|
| Ref_ptr &operator=(const Ref_ptr &rhs) { | | class node { |
| if(this!=&rhs) { | | T _val; |
| detach();
| | node *_left; |
| _c=rhs._c;
| | node *_right; |
| _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();
| |
| };
| |
| };
| |
| /* \href{mod11/code/ref_ptr.h}{ref\_ptr.h} */
| |
| </nowiki>
| |
|
| |
| ==auto\_ptr==
| |
|
| |
|
| Implementacja wskaźnika <code><nowiki> auto_ptr</nowiki></code> oparta jest o dwie funkcje.
| | node* _root; |
| Jedna zwalnia przechowywany wskaźnik zwracając go na zewnątrz i
| |
| oddając własność:
| |
|
| |
|
| <nowiki> T* release() {
| |
| T *oldPointee = pointee;
| |
| pointee = 0;
| |
| return oldPointee;
| |
| }
| |
| /* \href{mod11/code/auto_ptr.h}{auto\_ptr.h} */
| |
| </nowiki>
| |
|
| |
| <code><nowiki> pointee</nowiki></code> jest przechowywanym (zwykłym) wskaźnikiem.
| |
|
| |
| <nowiki> private:
| |
| T *pointee;
| |
| </nowiki>
| |
|
| |
| Druga funkcja zamienia przechowywany wskaźnik na inny, zwalnianiając
| |
| wskazywaną przez niego pamięć:
| |
|
| |
| <nowiki> void reset(T *p = 0) {
| |
| if (pointee != p) {
| |
| delete pointee;
| |
| pointee = p;
| |
| }
| |
| }
| |
| /* \href{mod11/code/auto_ptr.h}{auto\_ptr.h} */
| |
| </nowiki>
| |
|
| |
| Za pomocą tych funkcji można już łatwo zimplementować resztę szablonu np.:
| |
|
| |
| <nowiki> template<class T> class auto_ptr {
| |
| public:
| |
| explicit auto_ptr(T *p = 0): pointee(p) {}
| |
|
| |
| template<class U>
| |
| auto_ptr(auto_ptr<U>& rhs): pointee(rhs.release()) {}
| |
|
| |
| ~auto_ptr() { delete pointee; }
| |
|
| |
| template<class U>
| |
| auto_ptr<T>& operator=(auto_ptr<U>& rhs)
| |
| {
| |
| if (this != &rhs) reset(rhs.release());
| |
| return *this;
| |
| }
| |
|
| |
| T& operator*() const { return *pointee; }
| |
| T* operator->() const { return pointee; }
| |
| T* get() const { return pointee; }
| |
| } | | } |
| /* \href{mod11/code/auto_ptr.h}{auto\_ptr.h} */
| |
| </nowiki> | | </nowiki> |
|
| |
| Konstruktor kopiujacy i operator przypisania są szablonami. W ten
| |
| sposób można kopiować również wskaźniki <code><nowiki> auto_ptr</nowiki></code> opakowywujące
| |
| typy, które mogą być na siebie rzutowane np. można przypisać
| |
| <code><nowiki> auto_ptr<Derived></nowiki></code> do <code><nowiki> auto_ptr<Base></nowiki></code>, jeśli <code><nowiki> Derived</nowiki></code>
| |
| dziedziczy publicznie z <code><nowiki> Base</nowiki></code>. Konstruktor <code><nowiki> auto_ptr(T *p =
| |
| 0)</nowiki></code> został zadeklarowany jako <code><nowiki> explicit</nowiki></code>, wobec czego nie
| |
| spowoduje niejawnej konwersji z typu <code><nowiki> T*</nowiki></code> na <code><nowiki> auto_ptr<T></nowiki></code>.
| |
|
| |
| Różne impelentacje <code><nowiki> auto_ptr</nowiki></code> różnią się szczegułami dotyczącymi
| |
| obsługi <code><nowiki> const auto_ptr</nowiki></code> i przekazywania <code><nowiki> auto_ptr</nowiki></code> przez stałą
| |
| referencję. Powyższa implentacja wzięta, z \cite{MeyersMECPP}, nie
| |
| posiada pod tym względem żadnych zabezpieczeń. Szczegółowa dyskusja
| |
| tego zagadnienia i bardziej zaawansowana implementacja znajduje się w
| |
| \cite{josuttis}. Temat ten jest też poruszony w \cite{szablony}.
| |
| Warto też zaglądnąć do implementacji <code><nowiki> auto_ptr</nowiki></code> dostarczonej z
| |
| wraz z kompilatorem <code><nowiki> g++</nowiki></code>.</math>
| |