Zaawansowane CPP/Wykład 10: Inteligentne wskaźniki: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Matiunreal (dyskusja | edycje)
Nie podano opisu zmian
 
Nie podano opisu zmian
 
(Nie pokazano 163 wersji utworzonych przez 9 użytkowników)
Linia 1: Linia 1:
==Wstęp==
==Wstęp==


Wskaźniki jeden z bardziej pożytecznych elementów języka C/C++, ale
Wskaźniki to jeden z bardziej pożytecznych elementów języka C/C++, ale
napewno najbardziej niebezpieczny. Zabawa z gołymi wskaźnikami
na pewno najbardziej niebezpieczny. Zabawa z gołymi wskaźnikami
przypomina żónglerkę odbezpieczonymi granatami. To nie jest już
przypomina żonglerkę odbezpieczonymi granatami. To nie jest już
kwestia czy nastąpi wybuch, ale tego kiedy on nastąpi.  Możliwości
kwestia czy nastąpi wybuch, ale kiedy on nastąpi.  Możliwości
wywołania wybuchu i jego konsekwencje są wielorakie:
wywołania wybuchu i jego konsekwencje są wielorakie:
* Możemy usunąć wskaźnik nie zwalniając pamięci na którą on
* 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.
wskazuje.  Jeśli żaden inny wskaźnik nie wskazuje na ten obszar to
* Możemy zwolnić ten sam obszar pamięci kilkakrotnie. Powoduje to zwykle krach całej aplikacji.  
jest on bezpowrotnie stracony dla naszej aplikacji i mamy do
* Podobnie jeśli spróbujemy zdereferencjonować wskaźniki <tt>null</tt>.
czynienia z wyciekiem pamięci.
* 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.
* 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
Jeśli więc nie czujemy się jak Rambo, albo nie przymierzając sam Chuck
Norris, to powinniśmy poszukać jakiś zabezpieczeń. W C++
Norris, to powinniśmy poszukać jakichś zabezpieczeń. W C++
zabezpieczenia są dostarczane poprzez możliwość definicji własnych
zabezpieczenia są dostarczane poprzez możliwość definicji własnych
typów klas. Dzięki klasom możemy nie korzystać z dynamicznej alokacji
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
pamięci bezpośrednio, ale za pośrednictwem klas, które dbają o alokacje
w konstruktorach, dealokację w destruktorach, zwiększają i
w konstruktorach, dealokację w destruktorach, zwiększają i
zmmniejszają pamięć na żądanie, itp. Przykładem takiego podejścia sa
zmmniejszają pamięć na żądanie, itp. Przykładem takiego podejścia
np. kontenery STL, jedną z zalet których jest właśnie zarządzanie
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
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
rozważyć opakowanie ich w klasy.  Jest to możliwe dzięki możliwości
jakie w C++ daje przeładowywanie operatorów, w szczegolności możemy
jakie w C++ daje przeładowywanie operatorów, w szczególności możemy
przeładowywać operatory dereferencjonowania {operator*()} i
przeładowywać operatory dereferencjonowania <tt>operator*()</tt> i
{operator->()}. W ten sposób możemy upodobnić zachowanie
<tt>operator->()</tt>. W ten sposób możemy upodobnić zachowanie
definiowanych przez nas typów do zachowania wskaźników.
definiowanych przez nas typów do zachowania wskaźników.
Takie typy nazywamy inteligentnymi wskaźnikami, ponieważ dostarczają
Takie typy nazywamy inteligentnymi wskaźnikami, ponieważ dostarczają
nam dodatkowej funkcjonalności ponad zwykłe zachowanie wskaźnika.
nam dodatkowej funkcjonalności ponad zwykłe zachowanie wskaźnika.


Tak jak i u ludzi rodzaje inteligencji wskaźników bywają różne i
Tak jak i u ludzi, rodzaje inteligencji wskaźników bywają różne i
inteligente wskaźniki występują w najróżniejszych wariacjach. Podział
inteligentne wskaźniki występują w najróżniejszych wariacjach. Podział
tych wariantów można przeprowadzić na wiele sposobów, ja skoncentruję
tych wariantów można przeprowadzić na wiele sposobów, ja skoncentruję
sie na dwóch grupach:
sie na dwóch grupach:
* Zachowanie wskaźników podczas kopiowania, przypisywania  
* Zachowanie wskaźników podczas kopiowania, przypisywania i niszczenia. Nazwiemy to prawami własności.  
i niszczenia. Nazwiemy to prawami własności.  
* Zachowanie się operatorów  <tt>operator*()</tt> i <tt>operator->()</tt>. Nazwiemy to kontrolą dostępu.
* 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
Poniżej krótko przedstawię przegląd głównych możliwości w każdej z
Linia 51: Linia 42:
Głównym powodem używania inteligentnych wskaźników jest uzyskanie
Głównym powodem używania inteligentnych wskaźników jest uzyskanie
kontroli nad operacjami kopiowania, przypisywania i niszczenia
kontroli nad operacjami kopiowania, przypisywania i niszczenia
wskaźnika. W tym kontekście mówimy często że wskaźnik jest albo nie
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
jest właścicielem obiektu na który wskazuje.  Poniżej przedstawiam cztery
typowe schematy wskaźników.
typowe schematy wskaźników.
Linia 57: Linia 48:
====Głupie wskaźniki====
====Głupie wskaźniki====


Zwykłe (nieinteligentne) wskaźniki, nie są właścicielami obiektów na
{{kotwica|rys.10.1|}}
ktorę wskazują. Kopiowanie czy przypisanie prowadzi do współdzielenia
[[File:cpp-11-vulgaris.mp4|250x250px|thumb|right|Rysunek 10.1. Zwykłe wskaźniki.]]
 
Zwykłe (nieinteligentne) wskaźniki, nie są właścicielami obiektów, na
które wskazują. Kopiowanie czy przypisanie prowadzi do współdzielenia
referencji (oba wskaźniki wskazują na ten sam obiekt) często
referencji (oba wskaźniki wskazują na ten sam obiekt) często
niezamierzonej. Zniszczenie wskaźnika nie powoduje
niezamierzonej. Zniszczenie wskaźnika nie powoduje
zniszczenia (dealokacji pamięci) obiektu na który on wskazuje.
zniszczenia (dealokacji pamięci) obiektu, na który on wskazuje.
Przedstawia to rysunek&nbsp;[[##fig:ptr_vulgaris|Uzupelnic fig:ptr_vulgaris|]] na którym zilustrowano  
Przedstawia to [[#rys.10.1|rysunek 10.1]], na którym zilustrowano przebieg wykonania kodu:
przebieg wykonania kodu:


void f() {
void f() {
X *px( new X);
X *px( new X);
X  py(px);
X  py(px);
X  pz(new X);
X  pz(new X);
pz<nowiki>=</nowiki>py;
pz<nowiki>=</nowiki>py;
}
}<br>
 
f();
f();
 
[width<nowiki>=</nowiki>]{mod11/graphics/vulgaris.eps}
 
{Zwykłe wskaźniki.}


====Zliczanie referencji====
====Zliczanie referencji====


Wskaźniki zliczające referencje są niejako właścicielami grupowymi
Wskaźniki zliczające referencje są niejako właścicielami grupowymi
obiektu na który wskazują. Kopiowanie i przypisanie powoduje
obiektu, na który wskazują.
współdzielnie referencji, ale kontrolowane, w tym sensie że
{{kotwica|rys.10.2|}}[[File:cpp-11-reference.mp4|250x250px|thumb|right|Rysunek 10.2. Wskaźniki zliczające referencje.]]
monitorowana jest liczba wskaźników do danego obiektu. Na zasadzie
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
"ostatni gasi światło" zniszczenie wskaźnika powoduje zniszczenie
obiektu wtedy gdy był to jedyny (ostatni) wskaźnik na ten obiekt.
obiektu wtedy gdy był to jedyny (ostatni) wskaźnik na ten obiekt.
Liczenie referencji reprezentuje więc prostą wersję "odśmieczacza"
Liczenie referencji reprezentuje więc prostą wersję "odśmiecacza"
(garbage-collector). Zachowanie się tego typu wskaźników prezentuje
(garbage-collector). Zachowanie się tego typu wskaźników prezentuje
rysunek&nbsp;[[##fig:ptr_reference|Uzupelnic fig:ptr_reference|]] w oparciu o analogiczny kod.
[[#rys.10.2|rysunek 10.2]], 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<nowiki>=</nowiki>py;
}
 
f();


[width<nowiki>=</nowiki>]{mod11/graphics/reference.eps}
void f() {
 
ref_ptr<X>  px(new X);
{Wskaźniki zliczające referencje.}
ref_ptr<X>  py(px);
ref_ptr<X>  pz(new X);
pz<nowiki>=</nowiki>py;
}<br>
f();


====Głęboka/fizyczna kopia====
====Głęboka/fizyczna kopia====


Takie wskaźniki są pojedynczymi właścicielami obiektów na które
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ź
wskazują i zachowują się jak wartości, a nie wskaźniki.
Kopiowanie bądź
przypisanie powoduje fizyczne kopiowanie obiektu wskazywanego.
przypisanie powoduje fizyczne kopiowanie obiektu wskazywanego.
Zniszczenie wskaźnika powoduje zniczenie wskazywanego obiektu. Od
Zniszczenie wskaźnika powoduje zniszczenie wskazywanego obiektu. Od
zwykłych wartości obiektów różnią się tym, że maja zachowanie
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
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
więc nie możemy użyć bezpośrednio samych obiektów. Zachowanie kodu
{{kotwica|rys.10.3|}}[[File:cpp-11-deep_copy.mp4|250x250px|thumb|right|Rysunek 10.3. Wskaźniki wykonujące kopie fizyczne.]]
void f() {
clone_ptr<X>  px(new X);
clone_ptr<X>  py(px);
cloen_ptr<X>  pz(new X);
pz<nowiki>=</nowiki>py;
}<br>
f();


void f() {
ilustruje [[#rys.10.3|rysunek 10.3]].
clone_ptr<X>  px(new X);
clone_ptr<X>  py(px);
cloen_ptr<X>  pz(new X);
pz<nowiki>=</nowiki>py;
}
 
f();
 
ilustruje rysunek&nbsp;[[##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
Zastosowanie wskaźników z głębokim kopiowaniem zilustruję na
przykładzie podanego już wcześniej przykładu z kształtami
przykładzie podanego już wcześniej przykładu z kształtami
geometrycznymi.  W programie wykorzystującym takie kształty na pewno
geometrycznymi.  W programie wykorzystującym takie kształty na pewno
zachodzi konieczność kopiowania kształtów. Załóżmy że wybraliśmy
zachodzi konieczność kopiowania kształtów. Załóżmy, że wybraliśmy
(myszką) jakiś kształt i wskaźnik do niego jest przechowywany w
(myszką) jakiś kształt i wskaźnik do niego jest przechowywany w
zmiennej {Shape *selected}.  Załóżmy że jest to obiekt typy
zmiennej <tt>Shape *selected</tt>.  Załóżmy, że jest to obiekt typu
{Circle}.  Teraz chcemy uzyskać kopie tego kształtu. Przypisanie
<tt>Circle</tt>.  Teraz chcemy uzyskać kopię tego kształtu. Przypisanie


Shape *copy<nowiki>=</nowiki>selected;
Shape *copy<nowiki>=</nowiki>selected;


Oczywiście nie zadziała bo uzyskamy dwa wskaźniki na jeden obiekt. A
oczywiście nie zadziała, bo uzyskamy dwa wskaźniki na jeden obiekt. A
my potrzebujemy drugiego obiektu. Bez koniecznośći polimorfizmu
my potrzebujemy drugiego obiektu. Bez koniecznośći polimorfizmu
wystarczyłoby użyć konstruktora kopiującego:
wystarczyłoby użyć konstruktora kopiującego:


Shape *copy<nowiki>=</nowiki>new Shape(*selected);
Shape *copy<nowiki>=</nowiki>new Shape(*selected);


Niestety w naszym przypadku ten kod się nawet nie skompiluje, bo klasa
Niestety, w naszym przypadku ten kod się nawet nie skompiluje, bo klasa
{Shape}jest klasą abstrakcyjną. Nawet gdyby nie była to i tak
<tt>Shape</tt> jest klasą abstrakcyjną. Nawet gdyby nie była, to i tak
zostałby utworzony obiekt typu {Shape} a nie {Circle}.
zostałby utworzony obiekt typu <tt>Shape</tt>, a nie <tt>Circle</tt>.
W celu zaimpleemntowania kopiowania polimorficznego możemy wyposażyć naszą klasę {Shapew funkcje
W celu zaimplementowania kopiowania polimorficznego możemy wyposażyć naszą klasę <tt>Shape</tt> w funkcję


virtual Shape *clone() const <nowiki>=</nowiki> 0
virtual Shape *clone() const <nowiki>=</nowiki> 0


i następnie zdefiniować ją w każdej podklasie:
następnie zdefiniować ją w każdej podklasie:


class Circle :public Shape {
class Circle :public Shape {
...
...
Circle *clone() {return new Circle(*this);};
Circle *clone() {return new Circle(*this);};
}
}


i wtedy możemy skopiować (sklonować) nasz obiekt za pomocą  
i wtedy możemy skopiować (sklonować) nasz obiekt za pomocą  


Shape *copy <nowiki>=</nowiki> selected->clone();
Shape *copy <nowiki>=</nowiki> selected->clone();


Możemy teraz technikę, nazywaną również wzorcem prototypu, lub
Możemy teraz technikę, nazywaną również wzorcem prototypu lub
fabryką klonów {gamma}, zastosować w implementacji inteligentnego
fabryką klonów (zob. E. Gamma, R. Helm, R. Johnson, J. Vlissides <i>"Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku"</i>), zastosować w implementacji inteligentnego
wskaźnika {clone_ptr}
wskaźnika <tt>clone_ptr</tt>


clone_ptr<Shape> selected;
clone_ptr<Shape> selected;
...
...
clone_ptr<Shape> copy(selected);
clone_ptr<Shape> copy(selected);


====autoptr====
====auto_ptr====


Wskaźniki {auto_ptr} (jedyne inteligentne wskaźniki dostępne w
{{kotwica|rys.10.4|}}[[File:cpp-11-auto_ptr.mp4|250x250px|thumb|right|Rysunek 10.4. Wskaźniki <tt>auto_ptr</tt>.]]
standardzie C++) są pojedynczymi, bardzo zaborczymi, właścicielami obiektu
Wskaźniki <tt>auto_ptr</tt> (jedyne inteligentne wskaźniki dostępne w
na które wskazują. Tak zaborczymi że nie dopuszczają możliwości
standardzie C++) są pojedynczymi, bardzo zaborczymi, właścicielami obiektu,
współdzielenia obiektu, ani jego kopiowania.  Próba skopiowania, albo
na który wskazują. Tak zaborczymi, że nie dopuszczają możliwości
przypisania prowadzi do przekazania własności: Obiekt
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
kopiowany(przypisywany) oddaje/przekazuje prawo własności do
posiadanego obiektu, drugiemu obiektowi. Oznacza to że obiekt
posiadanego obiektu drugiemu obiektowi. Oznacza to, że obiekt
kopiowany lub przypisywany jest zmieniany w trakcie tych operacji.
kopiowany lub przypisywany jest zmieniany w trakcie tych operacji.
Ilustruje to rysunek&nbsp;[[##fig:auto_ptr|Uzupelnic fig:auto_ptr|]] na podstawie kodu
Ilustruje to [[#rys.10.4|rysunek 10.4]] na podstawie kodu


void f() {
void f() {
auto_ptr<X>  px(new X);
auto_ptr<X>  px(new X);
auto_ptr<X>  py(px);
auto_ptr<X>  py(px);
auto_ptr<X>  pz(new X);
auto_ptr<X>  pz(new X);
pz<nowiki>=</nowiki>py;
pz<nowiki>=</nowiki>py;
}
}<br>
 
f();
f();
 
[width<nowiki>=</nowiki>]{mod11/graphics/auto_ptr.eps}
 
{Wskaźniki <tt>autoptr</tt>.}


To bardzo nieintuicyjne zachowanie:
To bardzo nieintuicyjne zachowanie:
obiekty {auto_ptr} nie są modelami konceptu {Assignable}.
obiekty <tt>auto_ptr</tt> nie są modelami konceptu <tt>Assignable</tt>.
Wskaźniki te zostały wprowadzone, aby wspomagać bezpieczną alokację
Wskaźniki te zostały wprowadzone aby wspomagać bezpieczną alokację
zasobów (głownie pamięci), według wzorca "alokacja zasobów jest
zasobów (głównie pamięci) według wzorca "alokacja zasobów jest
inicjalizacją"{Stroustrup}. Rozważmy następujący przykład:
inicjalizacją" (zob. B. Stroustrup <i>"Język C++"</i>). Rozważmy następujący przykład:


int  f() {
int  f() {
BigX *p <nowiki>=</nowiki> new BigX;
  BigX *p <nowiki>=</nowiki> new BigX;
/*... tu coś się dzieje */
<i>... tu coś się dzieje</i>
delete  p;
  delete  p;
return wynik;
  return wynik;
}
}


Powyższy przykład to typowe zastosowanie dynamicznej alokacji pamięci.
Jest to typowe zastosowanie dynamicznej alokacji pamięci.
Problem polega na tym że jeżeli pomiędzy przydziałem pamięci jej
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ś
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.
to może być np. dodatkowe wyrażenie <tt>return</tt> lub rzucony wyjątek.
W obu przypadkach zniszczone zostana wszystkie statycznie zaalokowane
W obu przypadkach zniszczone zostaną wszystkie statycznie zaalokowane
obikety w tym i wskaźnik {p}. Ale ponieważ jest to zwykły wskaźnik
obiekty, w tym i wskaźnik <tt>p</tt>. Ale ponieważ jest to zwykły wskaźnik
jego zniszczenie nie spowoduje zwolnienia wskazywanej przez niego
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  
pamięci. Rozwiązaniem jest właśnie uczynienie go obiektem będącym właścicielem  
wskazywanej pamięci:
wskazywanej pamięci:


int  f() {
int  f() {
auto_ptr<BigX> p(new BigX);
  auto_ptr<BigX> p(new BigX);<br>
 
<i>... tu coś się dzieje</i><br>
/*... tu coś się dzieje */
  return wynik;
 
}
return wynik;
}


Teraz przy wyjściu z funkcji zostanie wywołany destruktor {p}, a on  
Teraz przy wyjściu z funkcji zostanie wywołany destruktor <tt>p</tt>, a on  
zwolni przydzieloną pamięc.
zwolni przydzieloną pamięć.


Wzkaźniki {auto_ptr} mogą być przekazywane i zwracane z funkcji.
Wzkaźniki <tt>auto_ptr</tt> mogą być przekazywane i zwracane z funkcji.
Jeśli przekażemy {auto_ptr} do funkcji przez wartość, to spowodowane
Jeśli przekażemy <tt>auto_ptr</tt> do funkcji przez wartość, to spowodowane
tym kopiowanie spowoduje, że własność zostanie przekazana na argument
tym kopiowanie spowoduje, że własność zostanie przekazana na argument
funkcji i pamięć zostanie zwolniona kiedy funkcja zakończy swoje działanie.
funkcji i pamięć zostanie zwolniona kiedy funkcja zakończy swoje działanie.


template<typename T> void val(T p) {
template<typename T> void val(T p) {
};
};<br>
auto_ptr<X> px(new X);
val(px);
<i>px zawiera wskaźnik null. pamięć jest zwolniona</i>
cout<<px.get()<<endl;
<i>zwraca opakowany wskaźnik na X, powinien być zero</i>


auto_ptr<X> px(new X);
Jeśli przekażemy <tt>auto_ptr</tt> przez referencje to kopiowania nie
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
będzie, przekazanie własności bedzie zależeć od tego czy wkaźnik
zostanie skopiowany lub przypisany wewnątrz funkcji.  
zostanie skopiowany lub przypisany wewnątrz funkcji.  


template<typename T> void ref_1(T &p) {
template<typename T> void ref_1(T &p) {
T x <nowiki>=</nowiki> p;
  T x <nowiki>=</nowiki> p;
};  
};  
template<typename T> void ref_2(T &p) {
template<typename T> void ref_2(T &p) {
};  
};<br>
auto_ptr<X> px(new X);
ref_2(px);
<i>nic sie nie zmieniło</i>
cout<<px.get()<<endl; <i>wypisuje jakiś adres</i>
ref_1(px)
<i>px zawiera wskaźnik null. pamięć jest zwolniona</i>
cout<<px.get()<<endl;
<i>zwraca opakowany wskaźnik na X, powinien być zero</i>


auto_ptr<X> px(new X);
W przypadku przekazania <tt>auto_ptr</tt> jako referencji do stałej sprawa
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
jest bardziej skomplikowana. Obecny standard stanowi, że wskaźnik
{auto_ptr} przekazany jako referencja do stałej, nie przekazuje
<tt>auto_ptr</tt> przekazany jako referencja do stałej, nie przekazuje
własności, tzn. operacje które by to tego prowadziły nie skompilują
własności, tzn. operacje, które by do tego prowadziły nie skompilują
się.  Z tych samych powodów nie powinien skompiluje się kod używający
się.  Z tych samych powodów nie powinien skompilować się kod używający
kontenerów STL zawierających wskaźniki {auto_ptr}.
kontenerów STL zawierających wskaźniki <tt>auto_ptr</tt>.
 
template<typename T> void cref_1(const T &p) {
T x <nowiki>=</nowiki> 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ę*/
template<typename T> void cref_1(const T &p) {
  T x <nowiki>=</nowiki> p;
};
template<typename T> void cref_2(const T &p) {
};<br>
auto_ptr<X> px(new X);
cref_2(px);
<i>OK, nic się nie stanie</i>
cout<<px.get()<<endl; <i>wypisuje jakiś adres</i>
cref_1(px) <i>nie skompiluje się</i><br>
std::vector<auto_ptr<X> > v(10); <i>nie skompiluje się</i>
([[media:Auto_ptr.cpp | Źródło: auto_ptr.cpp]])


Różne implementacje różnie sobie z tym radzą, i w praktyce wynik
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
kompilowania powyższych fragmentów kodu może być różny na różnych
platformach. Jest to dość techniczne zagadnienie, zainteresowane
platformach. Jest to dość techniczne zagadnienie, zainteresowane
osoby odsyłam do {szablony} i {josuttis}.
osoby odsyłam do D. Vandevoorde, N. Josuttis <i>"C++ Szablony, Vademecum profesjonalisty"</i> i N.M. Josuttis <i>"C++ Biblioteka standardowa, podręcznik programisty"</i>.


==Kontrola dostępu==
==Kontrola dostępu==
Linia 292: Linia 264:
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 {operator->()} i {operator*()}.  Wpływać na
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*() {
/* zrob coś */
<i>zrób coś</i>
return *_p;  
return *_p;  
}
}


Ten dodatkowy kod może np. sprawdzać czy wskaźnik {_p} nie jest
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 {*} i
Po drugie możemy zmienić zwracany typ. Wbudowane operatory <tt>*</tt> i
{->} zwracają odpowiednio {T&} i {T*}. Oczywiście my możemy
<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 {T&} i {T*}. Takie obiekty które "zachowują
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 316: Linia 288:
Dlaczego moglibyśmy chcieć używać obiektów zastępczych?  
Dlaczego moglibyśmy chcieć używać obiektów zastępczych?  


Typowe zastosowanie to implemenatacja operacji przypisania do obiektów
Typowe zastosowanie to implementacja 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
{ostream_iterator} dostarczany przez STL, który zezwala traktować
<tt>ostream_iterator</tt> dostarczany przez STL, który zezwala traktować
plik wyjściowy jak kontener z iteratorem typu {OutputIterator}:
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, "\n"));


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, "\n") iout;


to w zasadzie jedyną dozwoloną operacja jest przypisanie i zwiekszenie
to w zasadzie jedyną dozwoloną operacją jest przypisanie i zwiększenie
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
zwrócić. Możemy jednak zwrócić obiekt zastępczy, który będzie
zwrócić. Możemy jednak zwrócić obiekt zastępczy, który będzie
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) {
void operator<nowiki>=</nowiki>(const T &val) {
      _out<<val;
_out<<val;
    }  
}  
};
};
([[media:Out.cpp | Źródło: out.cpp]])


Tę klasę zamkniemy wewnątrz klasy {ostream_iterator}
Tę klasę zamkniemy wewnątrz klasy <tt>ostream_iterator</tt>


template<typename T> class ostream_iterator:  
template<typename T> class ostream_iterator:  
public std::iterator <std::output_iterator_tag, T >  {
public std::iterator <std::output_iterator_tag, T >  {<br>
  class writing_proxy {
  <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;};
};
([[media:Out.cpp | Źródło: out.cpp]])


class writing_proxy {
Dziedziczenie z klasy <tt>iterator</tt> zapewni nam, że nasz
/*...*/
<tt>ostream_iterator</tt> posiada wszystkie typy stowarzyszone wymagane
};
 
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
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
<tt>iterator_traits</tt>  (zob. [http://osilek.mimuw.edu.pl/index.php?title=Zaawansowane_CPP/Wyk%C5%82ad_5:_Klasy_cech#iterator_traits  wykład 5.5]). Bez tego nie moglibyśmy używać <tt>ostream_iterator</tt> w niektórych algorytmach STL.   
moglibyśmy używać {ostream_iterator} w niektórych algorytmach STL.   


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;
([[media:Out.cpp | Źródło: out.cpp]])


Wywołanie {*io} zwraca {writing_proxy}. Następnie wywoływany
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. Jeśli byśmy jednak
zachowanie, 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 {T}.
w obiekcie proxy zdefiniować operator konwersji na typ <tt>T</tt>.


operator T() {return T();}; /* uwaga bzdurny przykład !!! */
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 {i}. W ten sposób obiekty proxy
przypisze zero do zmiennej <tt>i</tt>. W ten sposób obiekty proxy
pozwalają nam rozróżniać użycie operatora {*} do odczytu i do
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 {gamma}). Poniżej przedstawię jeszcze jedną "sztuczkę"
(zob. E. Gamma, R. Helm, R. Johnson, J. Vlissides <i>"Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku"</i>). Poniżej przedstawię jeszcze jedną "sztuczkę"
opisaną w {alexandrescu} służącą do automatycznego obudowywania
opisaną w A. Alexandrescu <i>"Nowoczesne Projektowanie w C++"</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ływaniami innych funkcji.


===Opakowywanie wywołań funkcji===
===Opakowywanie wywołań funkcji===


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;}
}
}
([[media:Pre_post.cpp | Źródło: pre_post.cpp]])


Niech  
Niech  


Smart_prt<Widget> pw(new Widget);
Smart_prt<Widget> pw(new Widget);


bedzię inteligentnym wskaźnikiem do {Widget}.  
bedzię inteligentnym wskaźnikiem do <tt>Widget</tt>.  
Chcemy aby każde wywołanie funkcji z {Widget} np.
Chcemy aby każde wywołanie funkcji z <tt>Widget</tt> np.


pw->f1()
pw->f1()


zostało poprzedzone poprzez wywołanie funkcji {pre()}, a po nim
zostało poprzedzone przez wywołanie funkcji <tt>pre()</tt>, a po nim
nastapiło wywołanie funkcji {post()}. Jedną z możliwości jest
nastapiło wywołanie funkcji <tt>post()</tt>. Jedną z możliwości jest
oczywiście zmiana kodu funkcji {f?}, tak aby wywoływały na początku
oczywiście zmiana kodu funkcji <tt>f?</tt>, tak aby wywoływały na początku
{pre()} i {post()} na końcu. Można też dodać zestaw funkcji
<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 {Widget}.  
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();}
&nbsp;Wrapper()          {_p->post();}
  ~Wrapper()          {_p->post();}<br>
 
  T*  operator->() {return _p;}
T*  operator->() {return _p;}
};  
};  
([[media:Pre_post.cpp | Źródło: pre_post.cpp]])
 
W klasie inteligentnego wskaźnika przedefiniujemy {operator->()}
tak, aby zwracał {Wrapper<T>(T *)} zamiast{T*}.


template<typename T> struct Smart_ptr {
W klasie inteligentnego wskaźnika przedefiniujemy <tt>operator->()</tt>
T *_p;
tak, aby zwracał <tt>Wrapper<T>(T *)</tt> zamiast <tt>T*</tt>.
Smart_ptr(T *p):_p(p) {};
&nbsp;Smart_ptr(){delete _p;};


Wrapper<T> operator->()  {return Wrapper<T>(_p);};
template<typename T> struct Smart_ptr {
T &operator*() {return *_p};  
  T *_p;
};
  Smart_ptr(T *p):_p(p) {};
  ~Smart_ptr(){delete _p;};<br>
  Wrapper<T> operator->()  {return Wrapper<T>(_p);};
  T &operator*() {return *_p};  
};
([[media:Pre_post.cpp | Źródło: pre_post.cpp]])


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 {tmp} typu {Wrapper<Widget>} ale
operator ten zwraca obiekt <tt>tmp</tt> typu <tt>Wrapper<Widget></tt>, ale
najpier musi go skonstruować, a więc
najpierw 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 {operator->()} zwróci obiekt który posiada
 
{operator->()} to jest on wywoływany rekurencyjnie, aż zostanie
* 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  
{tmp.operator->()} który zwroci {p}.
* 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 {pw.operator->()} a więc zostanie
wywołany destruktor obiektu tymczasowego {tmp} który wywoła


p->post();
* 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();


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ę {f1()} za pomocą wyrażenia:
Jeśli 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
zaimplementować. Może to być traktowane jako wada, bo nie jestśmy w
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
stanie zapewnić, że każde wywołanie funkcji zostanie opakowane, ale z
drugiej strony mamy do dyspozycji możliwość wyboru pomiędzy opakowanym
drugiej strony mamy do dyspozycji możliwość wyboru pomiędzy opakowanym
i nieopakowanym wywołaniem funkcji.
i nieopakowanym wywołaniem funkcji.


==Współdzielenie reprezentacji==
===Współdzielenie reprezentacji===


Opisując inteligentne skaźniki nie można nie wspomnieć o technice
Opisując inteligentne wskaźniki nie można nie wspomnieć o technice
implementacyjnej, która jest ściśle z nimi zwiazana, a mianowicie o
implementacyjnej, która jest ściśle z nimi zwiazana, a mianowicie o
współdzieleniu reprezentacji. Technika ta polega na oddelegowaniu
współdzieleniu reprezentacji. Technika ta polega na oddelegowaniu
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&nbsp;[[##fig:cow|Uzupelnic fig:cow|]]).
do reprezentacji (zob [[#rys.10.5|rysunek 10.5]]).


{14cm}
{{kotwica|rys.10.5|}}[[File:cpp-10.5.svg|350x350px|thumb|right|Rysunek 10.5.]]


{}
class Wichajster {
 
public:
class Wichajster {
void do_something() {_rep->do_something();}
public:
private:
void do_something() {_rep->do_something();}
  WichajsterRep _rep;   
private:
}  
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 kopii obiektów klasy
{Wichajster} może współdzielić jedną reprezentację korzystając np.
<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 544: Linia 511:
W przypadku współdzielenia reprezentacji chcemy cały czas rozróżniać
W przypadku współdzielenia reprezentacji chcemy cały czas rozróżniać
obiekty, współdzielenie jest tylko technicznym środkiem optymalizacji.
obiekty, współdzielenie jest tylko technicznym środkiem optymalizacji.
Wymaga to zastosowania techniki "copy on write", tzn. w momencie w
Wymaga to zastosowania techniki "copy on write", tzn. w momencie, w
którym dokonujemy na obiekcie operacji mogącej go zmienić i jeśli
którym dokonujemy na obiekcie operacji mogącej go zmienić i jeśli
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&nbsp;[[##fig:cow|Uzupelnic fig:cow|]].
na [[#rys.10.5|rysunku 10.5]].
 
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,
problem jest tylko z metodami, które zwracaja referencje do wnętrza
problem jest 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.
obiektu. Takie metody mogą służyć zarówno do zapisy jak i do odczytu.
Częś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 {MeyersMECPP}.
znajduje się w S. Meyers <i>"Język C++ bardziej efektywny"</i>.


==Iteratory==
==Iteratory==
Linia 561: Linia 527:
Iteratory to kolejny rodzaj inteligentnych wskaźników. Jeżli chodzi o
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
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
zwykłe wskaźniki. Wyjątkiem są specjalne iteratory, takie jak
{ostream_iterator}, czy {back_inserter}, wspomniane powyżej.
<tt>ostream_iterator</tt>, czy <tt>back_inserter</tt>, wspomniane powyżej.
Ale zasadniczo inteligencja iteratorów umiejscowiona jest w operacjach
Ale zasadniczo inteligencja iteratorów umiejscowiona jest w operacjach
arytmetycznych. Chodzi głównie o operator {operator++()} ponieważ
arytmetycznych. Chodzi głównie o operator <tt>operator++()</tt> ponieważ
wyposażone są w niego wszystkie iteratory kontenerów z STL. To
wyposażone są w niego wszystkie iteratory kontenerów z STL. To
właśnie jest zresztą podstawowa rola iteratora: przechodzenie do
właśnie jest zresztą podstawowa rola iteratora: przechodzenie do
kolejnych elementów, semantyka wskaźnika do już wybór twórcow STL.
kolejnych elementów, semantyka wskaźnika to już wybór twórcow STL.


==Implementacje==
==Implementacje==


Widać że różnorodność inteligentnych wskaźników może przyprawić o
Widać, że różnorodność inteligentnych wskaźników może przyprawić o
zawrót głowy. A nie rozważyliśmy jeszcze wszystkich kwestii
zawrót głowy. A nie rozważyliśmy jeszcze wszystkich kwestii
dotyczących ich zachowania. Wyczerpująca dyskusja na ten
dotyczących ich zachowania. Wyczerpująca dyskusja na ten
temat znajduje się w {alexandrescu}. Tam też podana jest
temat znajduje się w A. Alexandrescu <i>"Nowoczesne projektowanie"</i>. Tam też podana jest
implemenatcja uniwersalnego szablony klasy inteligentnego wskaźnika
implemenatcja uniwersalnego szablonu klasy inteligentnego wskaźnika
parametryzowanego kilkoma klasami wytycznymi. Alternatywą jest użycie
parametryzowanego kilkoma klasami wytycznymi. Alternatywą jest użycie
szeregu klas (szablonów) implementujacych jeden typ wskaźnika każda.
szeregu klas (szablonów) implementujacych jeden typ wskaźnika każda.
Zbiór takich klas można znaleźć w bibliotece {boost} ({}).
Zbiór takich klas można znaleźć w bibliotece <tt>boost()</tt>.
Bardzo dobre opisy implementacji inteligentnych wskaźników znajdują
Bardzo dobre opisy implementacji inteligentnych wskaźników znajdują
się również w {szablony} i {MeyersMECPP}.
się również w D. Vandevoorde, N. Josuttis: <i>"C++ Szablony, Vademecum profesjonalisty"</i> i S. Meyers <i>"Język C++ bardziej efektywny"</i>.


Tutaj dla przykładu zaprezentuję implementację wskaźnika zliczającego
Tutaj dla przykładu zaprezentuję implementację wskaźnika zliczającego
referencję paramtryzowanego jedną klasą wytyczną. Jest to podejście
referencję paramtryzowanego jedną klasą wytyczną. Jest to podejście
zbliżone do {szablony}.
zbliżone do D. Vandevoorde, N. Josuttis: <i>"C++ Szablony, Vademecum profesjonalisty"</i>.


==Zliczanie  referencji==
==Zliczanie  referencji==


Implementacje zliczanie referencji różnią się przede wszystkim
Implementacje zliczania referencji różnią się przede wszystkim
miejscem w którym umieszczony zostanie licznik. Dwie główne możliwości
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
to wewnątrz lub na zewnątrz obiektu, na który wskazujemy. Pierwsza
możliwość jest ewidentnie możliwa tylko wtedy, jeśli mamy dostęp do
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.
kodu tej klasy. W każdej z tych dwu grup  mamy dalsze możliwości, np.


[width<nowiki>=</nowiki>]{mod11/graphics/counter.eps}
{{kotwica|rys.10.6|}}[[File:cpp-11-counter.svg|350x500px|thumb|right|Rysunek 10.6. Wskaźniki ze współdzieleniem referencji.]]


{Wkaźniki ze współdzileniem referencji.}
# Obiekt wskazywany udostępnia miejsce na licznik, zarządzaniem licznikiem zajmuje się wskaźnik
# Obiekt wskazywany udostępnia miejsce na licznik, zarządzaniem licznikiem
# Obiekt wskazywany udostępnia nie tylko licznik, ale i interfejs do zarządzania nim.
zajmuje się wskaźnik
# {{kotwica|prz.3|}}Licznik jest osobnym obiektem. Każdy wskaźnik posiada wskaźnik na obiekt wskazywany i wskaźnik na licznik (zob. [[#rys.10.6|rysunek 10.6]]).
# Obiekt wskazywany udostępnia nie tylko licznik, ale i interfejs do  
#{{kotwica|prz.4|}} Licznik jest osobnym obiektem który zawiera również wskaźnik do obiektu wskazywanego. Każdy wskaźnik zawiera tylko wskaźnik do licznika (zob. [[#rys.10.6|rysunek 10.6]]).
zarządzania nim.
# Nie ma licznika, wskaźniki do tego samego obiektu połączone są w listę (zob. [[#rys.10.6|rysunek 10.6]]).  
# Licznik jest osobnym obiektem. Każdy wskaźnik posiada wskaźnik
na obiekt wskazywany i wskaźnik na licznik (zobacz
rysunek&nbsp;[[##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&nbsp;[[##fig:counter|Uzupelnic fig:counter|]]).
# Nie ma licznika, wskaźniki do tego samego obiektu połączone są w
listę (zobacz rysunek&nbsp;[[##fig:counter|Uzupelnic fig:counter|]]).  


Pokażę teraz przykladową implementację szablonu wskaźnika
Pokażę teraz przykladową implementację szablonu wskaźnika
parametryzowanego jedną klasą wytyczną określającą którąś z powyższych
parametryzowanego jedną klasą wytyczną, określającą którąś z powyższych
strategii, aczkolwiek przy jednej wytycznej jest to wysiłek ktory
strategii, aczkolwiek przy jednej wytycznej jest to wysiłek, który
pewnie sie nie opłaca, jako że kod wspólny jest dość mały. Ale
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
implementacja ta może stanowić podstawę do rozszerzenia o kolejne
wytyczne.  
wytyczne.  


Najpierw musimy się zastanowić na interfejsem lub raczej konceptem
Najpierw musimy się zastanowić nad interfejsem lub raczej konceptem
klasy wytycznej. W sumie najłatwiej to zrobić implemetując konkretną
klasy wytycznej. W sumie najłatwiej to zrobić implemetując konkretną
wytyczną. Zaczniemy od osobnego zewnętrznego licznika
wytyczną. Zaczniemy od osobnego zewnętrznego licznika (zob. [[#prz.3|strategia 3]]). Klasa wytyczna musi zawierać wskaźnik do wspólnego licznika:
(strategia&nbsp;[[##lbl:external|Uzupelnic lbl:external|]]).
Klasa wytyczna musi zawierać wsaźnik do wspólnego licznika:


template<typename T> struct Extra_counter_impl {
template<typename T> struct Extra_counter_impl {
/*...*/
<i>...</i>
private:
private:
size_t *_c;
  size_t *_c;
};
};


i funkcje zwiekszające i zmniejszające licznik:
i funkcje zwiekszające i zmniejszające licznik:


public:
  public:
bool remove_ref()    {--(*_c);return *_c<nowiki>=</nowiki><nowiki>=</nowiki>0;};
  bool remove_ref()    {--(*_c);return *_c<nowiki>=</nowiki><nowiki>=</nowiki>0;};
void add_ref()      {++(*_c);};
  void add_ref()      {++(*_c);};
size_t count() {return *_c;};
  size_t count() {return *_c;};
};
};


Funkcja zmniejszająca licznik zwraca prawdę, jeśli usunięta została
Funkcja zmniejszająca licznik zwraca prawdę, jeśli usunięta została
ostatnia referencja do wskazywanego obiektu. Potrzebna też będzie
ostatnia referencja do wskazywanego obiektu. Potrzebna też będzie
funkcja niszcząca licznik:  
funkcja niszcząca licznik:


void cleanup() {
void cleanup() {
delete _c;
  delete _c;
_c<nowiki>=</nowiki>0;
  _c<nowiki>=</nowiki>0;
}
}


Potrzebne będą dwa konstruktory: defaultowy, który nic nie robi:  
Potrzebne będą dwa konstruktory: defaultowy, który nic nie robi:  


Extra_counter_impl():_c(0)                    {};
Extra_counter_impl():_c(0)                    {};


i konstruktor inicjalizujący licznik obiektu powstającego po raz pierwszy:
i konstruktor inicjalizujący licznik obiektu powstającego po raz pierwszy:


Extra_counter_impl(T* p):_c(new size_t) {*_c<nowiki>=</nowiki>0;};
Extra_counter_impl(T* p):_c(new size_t) {*_c<nowiki>=</nowiki>0;};


który przydziela pamięć dla licznika. Argument {T* p} służy tylko
który przydziela pamięć dla licznika. Argument <tt>T* p</tt> służy tylko
do rozróżnienia tych konstruktorów.  
do rozróżnienia tych konstruktorów.  


Korzystając z tej klasy nietrudno jest napsiać szablon inteligentnego
Korzystając z tej klasy nietrudno jest napisać szablon inteligentnego
wskaźnika. Obiekt licznika może być składować tego szablonu lub możemy
wskaźnika. Obiekt licznika może być składową tego szablonu lub możemy
dziedziczyć z klasy wytycznej (zob&nbsp;[[##|Uzupelnic |]]). Niestety okazę się że
dziedziczyć z klasy wytycznej (zob. [http://osilek.mimuw.edu.pl/index.php?title=Zaawansowane_CPP/Wyk%C5%82ad_7:_Klasy_wytycznych wykład 7]). Niestety, okaże się, że 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 (zob. [[#prz.4|strategia 4]]). Dlatego zmienimy trochę naszą implementację wytycznej i założymy, że obiekty tej klasy będą zawierać również wskaźnik na obiekt wskazywany  
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&nbsp;[[##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.


T *_p;
T *_p;


i dodamy funkcję  
i dodamy funkcję:


T* pointee() {return _p;}
T* pointee() {return _p;}


Musimy jeszcze poprawić funkcję czyszczącą  
Musimy jeszcze poprawić funkcję czyszczącą:


void cleanup() {
void cleanup() {
delete _c;
  delete _c;
delete _p;
  delete _p;
_p<nowiki>=</nowiki>0;
  _p<nowiki>=</nowiki>0;
}
}
([[media:Ref_ptr.h | Źródło: ref_ptr.h]])


I jeden z konstruktorów:
i jeden z konstruktorów:


Extra_counter_impl(T* p):_c(new size_t),_p(p) {*_c<nowiki>=</nowiki>0;};
Extra_counter_impl(T* p):_c(new size_t),_p(p) {*_c<nowiki>=</nowiki>0;};


Szablon wskaźnika korzystający z tak zdefiniowanej klasy wytycznej może  
Szablon wskaźnika korzystający z tak zdefiniowanej klasy wytycznej może  
wyglądać następująco:
wyglądać następująco:


template<typename T,
template<typename T,
typename counter_impl <nowiki>=</nowiki> Extra_counter_impl<T>  >  
          typename counter_impl <nowiki>=</nowiki> Extra_counter_impl<T>  >  
class Ref_ptr {
class Ref_ptr {
public:
public:
Ref_ptr() {};
  Ref_ptr() {};
Ref_ptr(T *p):_c(p) {
  Ref_ptr(T *p):_c(p) {
_c.add_ref();
    _c.add_ref();
};
  };<br>
 
  ~Ref_ptr() {detach();}<br>
&nbsp;Ref_ptr() {detach();}
  Ref_ptr(const Ref_ptr &p):_c(p._c) {
 
    _c.add_ref();
Ref_ptr(const Ref_ptr &p):_c(p._c) {
  }<br>
_c.add_ref();
  Ref_ptr &operator<nowiki>=</nowiki>(const Ref_ptr &rhs) {
}
    if(this!<nowiki>=</nowiki>&rhs) {
 
      detach();
Ref_ptr &operator<nowiki>=</nowiki>(const Ref_ptr &rhs) {
      _c<nowiki>=</nowiki>rhs._c;
if(this!<nowiki>=</nowiki>&rhs) {
      _c.add_ref();
detach();
    }
_c<nowiki>=</nowiki>rhs._c;
    return *this;
_c.add_ref();
  }<br>
}
  T* operator->() {return _c.pointee();}
return *this;
  T &operator*()  {return *(_c.pointee());}<br>
}
  size_t count() {return _c.count();};
 
private:
T* operator->() {return _c.pointee();}
  mutable counter_impl _c;
T &operator*()  {return *(_c.pointee());}
  void detach() {       
 
    if (_c.remove_ref() ) _c.cleanup();
size_t count() {return _c.count();};
  };
private:
};
mutable counter_impl _c;
([[media:Ref_ptr.h | Źródło: ref_ptr.h]])
void detach() {       
if (_c.remove_ref() ) _c.cleanup();
};
};


==autoptr==
==auto_ptr==


Implementacja wskaźnika {auto_ptr} oparta jest o dwie funkcje.
Implementacja wskaźnika <tt>auto_ptr</tt> oparta jest o dwie funkcje.
Jedna zwalnia przechowywany wskaźnik zwracając go na zewnątrz i
Jedna zwalnia przechowywany wskaźnik zwracając go na zewnątrz i
oddając własność:
oddając własność:


T* release()  {
T* release()  {
T *oldPointee <nowiki>=</nowiki> pointee;
  T *oldPointee <nowiki>=</nowiki> pointee;
pointee <nowiki>=</nowiki> 0;
  pointee <nowiki>=</nowiki> 0;
return oldPointee;
  return oldPointee;
}
}
([[media:Auto_ptr.h | Źródło: auto_ptr.h]])


{pointee} jest przechowywanym (zwykłym) wskaźnikiem.
<tt>pointee</tt> jest przechowywanym (zwykłym) wskaźnikiem.


private:
private:
T *pointee;
  T *pointee;


Druga funkcja zamienia przechowywany wskaźnik na inny, zwolaniając
Druga funkcja zamienia przechowywany wskaźnik na inny, zwalniając
wskazywaną przez niego pamięć:  
wskazywaną przez niego pamięć:  


void reset(T *p <nowiki>=</nowiki> 0)  {
void reset(T *p <nowiki>=</nowiki> 0)  {
if (pointee !<nowiki>=</nowiki> p) {
  if (pointee !<nowiki>=</nowiki> p) {
delete pointee;
    delete pointee;
pointee <nowiki>=</nowiki> p;
    pointee <nowiki>=</nowiki> p;
}
  }
}
}
 
([[media:Auto_ptr.h | Źródło: auto_ptr.h]])
Za pomocą tych funkcji można już łatwo zimplementować resztę szablonu np.:


template<class T> class auto_ptr {
Za pomocą tych funkcji można już łatwo zimplementować resztę szablonu, np.:
public:
explicit auto_ptr(T *p <nowiki>=</nowiki> 0): pointee(p) {}


template<class U>
template<class T> class auto_ptr {
auto_ptr(auto_ptr<U>& rhs): pointee(rhs.release()) {}
public:
  explicit auto_ptr(T *p <nowiki>=</nowiki> 0): pointee(p) {}<br>
  template<class U>
  auto_ptr(auto_ptr <nowiki><U></nowiki> & rhs): pointee(rhs.release()) {}<br>
  ~auto_ptr() { delete pointee; }<br>
  template<class U>
  auto_ptr<T>& operator<nowiki>=</nowiki>(auto_ptr<nowiki><U></nowiki>& rhs)
  {
  if (this !<nowiki>=</nowiki> &rhs) reset(rhs.release());
  return *this;
  }<br>
  T& operator*() const { return *pointee; }
  T* operator->() const { return pointee; }
  T* get() const { return pointee; }
}
([[media:Auto_ptr.h | Źródło: auto_ptr.h]])


&nbsp;auto_ptr() { delete pointee; }
Konstruktor kopiujący i operator przypisania są szablonami, w ten
sposób można kopiować również wskaźniki <tt>auto_ptr</tt> opakowujące
typy, które mogą być na siebie rzutowane, np. można przypisać
<tt>auto_ptr<Derived></tt> do <tt>auto_ptr<Base></tt>, jeśli <tt>Derived</tt>
dziedziczy publicznie z <tt>Base</tt>. Konstruktor <tt>auto_ptr(T *p <nowiki>=</nowiki>
0)</tt> został zadeklarowany jako <tt>explicit</tt>, wobec czego nie
spowoduje niejawnej konwresji z typu <tt>T*</tt> na <tt>auto_ptr<T></tt>.


template<class U>  
Różne impelentacje <tt>auto_ptr</tt> różnią się szczegółami dotyczącymi
auto_ptr<T>& operator<nowiki>=</nowiki>(auto_ptr<U>& rhs)
obsługi <tt>const auto_ptr</tt> i przekazywania <tt>auto_ptr</tt> przez stałą
{
referencję. Powyższa implentacja wzięta z S. Meyers <i>"Język C++ bardziej efektywny"</i>, nie posiada pod tym względem żadnych zabezpieczeń. Szczegółowa dyskusja
if (this !<nowiki>=</nowiki> &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 <nowiki>=</nowiki>
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
tego zagadnienia i bardziej zaawansowana implementacja znajduje się w
{josuttis}. Temat ten jest też poruszony w {szablony}.
N.M. Josuttis: <i>"C++ Biblioteka standardowa, podręcznik programisty"</i>. Temat ten jest też poruszony w D. Vandevoorde, N. Josuttis: <i>"C++ Szablony, Vademecum profesjonalisty"</i>.
Warto też zaglądnąć do implementacji {auto_ptr} dostarczonej z
Warto też zaglądnąć do implementacji <tt>auto_ptr</tt> dostarczonej z
kompilatorem {g++}.
kompilatorem <tt>g++</tt>.
 
==Podsumowanie==

Aktualna wersja na dzień 14:07, 3 paź 2021

Wstęp

Wskaźniki to jeden z bardziej pożytecznych elementów języka C/C++, ale na pewno najbardziej niebezpieczny. Zabawa z gołymi wskaźnikami przypomina żonglerkę odbezpieczonymi granatami. To nie jest już kwestia czy nastąpi wybuch, ale 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 zmieniając nie te zmienne co trzeba.

Jeśli więc nie czujemy się jak Rambo, albo nie przymierzając sam Chuck Norris, to powinniśmy poszukać jakichś 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 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ści jakie w C++ daje przeładowywanie operatorów, w szczególnoś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 inteligentne 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.

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.

Głupie wskaźniki

Rysunek 10.1. Zwykłe wskaźniki.

Zwykłe (nieinteligentne) wskaźniki, nie są właścicielami obiektów, na które 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, na którym zilustrowano przebieg wykonania kodu:

void f() {
X *px( new X);
X  py(px);
X  pz(new X);
pz=py;
}
f();

Zliczanie referencji

Wskaźniki zliczające referencje są niejako właścicielami grupowymi obiektu, na który wskazują.

Rysunek 10.2. Wskaźniki zliczające referencje.

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śmiecacza" (garbage-collector). Zachowanie się tego typu wskaźników prezentuje rysunek 10.2, 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();

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

Rysunek 10.3. Wskaźniki wykonujące kopie fizyczne.
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.

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 typu Circle. Teraz chcemy uzyskać kopię 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 zaimplementowania kopiowania polimorficznego możemy wyposażyć naszą klasę Shape w funkcję

virtual Shape *clone() const = 0

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 (zob. E. Gamma, R. Helm, R. Johnson, J. Vlissides "Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku"), zastosować w implementacji inteligentnego wskaźnika clone_ptr

clone_ptr<Shape> selected;
...
clone_ptr<Shape> copy(selected);

auto_ptr

Plik:Cpp-11-auto ptr.mp4
Rysunek 10.4. Wskaźniki auto_ptr.

Wskaźniki auto_ptr (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 10.4 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();

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łównie pamięci) według wzorca "alokacja zasobów jest inicjalizacją" (zob. B. Stroustrup "Język C++"). Rozważmy następujący przykład:

int  f() {
  BigX *p = new BigX;
... tu coś się dzieje
  delete  p;
  return wynik;
}

Jest 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 return lub rzucony wyjątek. W obu przypadkach zniszczone zostaną wszystkie statycznie zaalokowane obiekty, 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, powinien 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, powinien 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 do tego prowadziły nie skompilują się. Z tych samych powodów nie powinien skompilować 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ódło: auto_ptr.cpp)

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. Vandevoorde, N. Josuttis "C++ Szablony, Vademecum profesjonalisty" i N.M. Josuttis "C++ Biblioteka standardowa, podręcznik programisty".

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*() {
zrób 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).

Proxy

Dlaczego moglibyśmy chcieć używać obiektów zastępczych?

Typowe zastosowanie to implementacja 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, "\n"));

Przypatrzny się temu przykładowi bliżej. Jeśli zdefiniujemy

ostream_iterator<int>(cout, "\n") iout;

to w zasadzie jedyną dozwoloną operacją jest przypisanie i zwiększenie 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; } };

( Źródło: out.cpp)

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

( Źródło: out.cpp)

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 (zob. wykład 5.5). 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;

( Źródło: out.cpp)

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, 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 (zob. E. Gamma, R. Helm, R. Johnson, J. Vlissides "Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku"). Poniżej przedstawię jeszcze jedną "sztuczkę" opisaną w A. Alexandrescu "Nowoczesne Projektowanie w C++", służącą do automatycznego obudowywania funkcji wywoływanych za pośrednictwem inteligentnego wskaźnika wywoływaniami innych funkcji.

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

( Źródło: pre_post.cpp)

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 przez 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;} };

( Źródło: pre_post.cpp)

W klasie inteligentnego wskaźnika przedefiniujemy operator->() tak, aby zwracał Wrapper<T>(T *) zamiast T*.

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

( Źródło: pre_post.cpp)

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 najpierw 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. Jeśli 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 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 wskaź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).

Rysunek 10.5.
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 kopii 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. 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 zapisu, jak i do odczytu. Częś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 S. Meyers "Język C++ bardziej efektywny".

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 to już wybór twórcow 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 A. Alexandrescu "Nowoczesne projektowanie". 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 boost(). Bardzo dobre opisy implementacji inteligentnych wskaźników znajdują się również w D. Vandevoorde, N. Josuttis: "C++ Szablony, Vademecum profesjonalisty" i S. Meyers "Język C++ bardziej efektywny".

Tutaj dla przykładu zaprezentuję implementację wskaźnika zliczającego referencję paramtryzowanego jedną klasą wytyczną. Jest to podejście zbliżone do D. Vandevoorde, N. Josuttis: "C++ Szablony, Vademecum profesjonalisty".

Zliczanie referencji

Implementacje zliczania 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. Pierwsza 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.

Rysunek 10.6. Wskaźniki ze współdzieleniem referencji.
  1. Obiekt wskazywany udostępnia miejsce na licznik, zarządzaniem licznikiem zajmuje się wskaźnik
  2. Obiekt wskazywany udostępnia nie tylko licznik, ale i interfejs do zarządzania nim.
  3. Licznik jest osobnym obiektem. Każdy wskaźnik posiada wskaźnik na obiekt wskazywany i wskaźnik na licznik (zob. rysunek 10.6).
  4. Licznik jest osobnym obiektem który zawiera również wskaźnik do obiektu wskazywanego. Każdy wskaźnik zawiera tylko wskaźnik do licznika (zob. rysunek 10.6).
  5. Nie ma licznika, wskaźniki do tego samego obiektu połączone są w listę (zob. rysunek 10.6).

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, który pewnie sie 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ć nad interfejsem lub raczej konceptem klasy wytycznej. W sumie najłatwiej to zrobić implemetując konkretną wytyczną. Zaczniemy od osobnego zewnętrznego licznika (zob. strategia 3). Klasa wytyczna musi zawierać wskaź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 napisać szablon inteligentnego wskaźnika. Obiekt licznika może być składową tego szablonu lub możemy dziedziczyć z klasy wytycznej (zob. wykład 7). Niestety, okaże się, że 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 (zob. strategia 4). 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;
}

( Źródło: ref_ptr.h)

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(); }; };

( Źródło: ref_ptr.h)

auto_ptr

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

( Źródło: auto_ptr.h)

pointee jest przechowywanym (zwykłym) wskaźnikiem.

private:
  T *pointee;

Druga funkcja zamienia przechowywany wskaźnik na inny, zwalniając wskazywaną przez niego pamięć:

void reset(T *p = 0)  {
  if (pointee != p) {
    delete pointee;
    pointee = p;
  }
}

( Źródło: auto_ptr.h)

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

( Źródło: auto_ptr.h)

Konstruktor kopiujący i operator przypisania są szablonami, w ten sposób można kopiować również wskaźniki auto_ptr opakowują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ę szczegółami dotyczącymi obsługi const auto_ptr i przekazywania auto_ptr przez stałą referencję. Powyższa implentacja wzięta z S. Meyers "Język C++ bardziej efektywny", nie posiada pod tym względem żadnych zabezpieczeń. Szczegółowa dyskusja tego zagadnienia i bardziej zaawansowana implementacja znajduje się w N.M. Josuttis: "C++ Biblioteka standardowa, podręcznik programisty". Temat ten jest też poruszony w D. Vandevoorde, N. Josuttis: "C++ Szablony, Vademecum profesjonalisty". Warto też zaglądnąć do implementacji auto_ptr dostarczonej z kompilatorem g++.