|
|
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>
| |