Zaawansowane CPP/Wykład 15: Wyjątkowo odporny kod: Różnice pomiędzy wersjami
Nie podano opisu zmian |
|||
(Nie pokazano 49 wersji utworzonych przez 5 użytkowników) | |||
Linia 1: | Linia 1: | ||
==Wstęp== | ==Wstęp== | ||
Linia 9: | Linia 4: | ||
wyjątków. Jest to bardzo silny mechanizm: rzucony wyjątek powoduje | wyjątków. Jest to bardzo silny mechanizm: rzucony wyjątek powoduje | ||
natychmiastowe przekazanie sterowania do najbliższej klauzuli | natychmiastowe przekazanie sterowania do najbliższej klauzuli | ||
<code><nowiki>catch</nowiki></code>, niejako "tnąc" w poprzek dowolnie | <code><nowiki> catch</nowiki></code>, niejako "tnąc" w poprzek dowolnie głęboko zagnieżdżone | ||
funkcje. To oczywiście jest jedną z jego podstawowych zalet, ale musimy | |||
podchodzić do tej własności bardzo ostrożnie. | podchodzić do tej własności bardzo ostrożnie. | ||
Linia 19: | Linia 14: | ||
W zasadzie korzystanie z wyjątków jest proste: funkcja, która stwierdza | W zasadzie korzystanie z wyjątków jest proste: funkcja, która stwierdza | ||
wystąpienie | wystąpienie błędu, a nie umie go sama obsłużyć, przekazuje | ||
odpowiedzialność swoim przełożonym, rzucając wyjątek. Jej | odpowiedzialność swoim przełożonym, rzucając wyjątek. Jej | ||
przełożeni mogą zrobić to samo(wystarczy, że nie przechwycą wyjątku). | przełożeni mogą zrobić to samo (wystarczy, że nie przechwycą wyjątku). | ||
Zakładamy jednak, że gdzieś w tej | Zakładamy jednak, że gdzieś w tej hierarchii wyjątek zostanie złapany przez | ||
kogoś, kto wie jak go obsłużyć. W praktyce sprawa może być bardziej | kogoś, kto wie jak go obsłużyć. W praktyce sprawa może być bardziej | ||
skomplikowana. Rzucony wyjątek powoduje natychmiastowe przerwanie | skomplikowana. Rzucony wyjątek powoduje natychmiastowe przerwanie | ||
nie tylko funkcji, która go rzuciła, ale również wszystkich funkcji | nie tylko funkcji, która go rzuciła, ale również wszystkich funkcji, | ||
przez | przez które "przelatuje". Jeśli te funkcje nie są na to | ||
przygotowane, to wyjątek może narobić dodatkowych szkód. | przygotowane, to wyjątek może narobić dodatkowych szkód. | ||
Typowy przykład to niezwolnione zasoby: | Typowy przykład to niezwolnione zasoby: | ||
void f() { | |||
przydziel_zasob(); | przydziel_zasob(); | ||
g(); /*może rzucić wyjątek*/ | g(); /*może rzucić wyjątek*/ | ||
zwolnij_zasob(); | zwolnij_zasob(); | ||
} | } | ||
Rzucenie wyjątku z <code>g()</code> spowoduje wyciek zasobu (zwykle pamięci). Taki przykład był już rozważany w [[Zaawansowane CPP/Wykład 10: Inteligentne wskaźniki|Wykładzie 10]] | |||
Rzucenie wyjątku z <code | Podane tam rozwiązanie to technika "przydział zasobu jest | ||
Taki przykład był już rozważany w | |||
Podane tam rozwiązanie | |||
inicjalizacją", czyli oddelegowanie zarządzania zasobem do osobnej | inicjalizacją", czyli oddelegowanie zarządzania zasobem do osobnej | ||
klasy, której konstruktor | klasy, której konstruktor przydziela zasób, a destruktor zwalnia: | ||
void f() { | |||
Zasob x; | |||
g(); /*może rzucić wyjątek*/ | |||
} /* niejawnie wywoływany destruktor x. Zasob() */ | |||
Wtedy podczas zwijania stosu zasób zostanie zwolniony automatycznie. | |||
Wtedy podczas | |||
Proszę zauważyć jednak, że jeśli nie przechwycimy wyjątku, to zasób | Proszę zauważyć jednak, że jeśli nie przechwycimy wyjątku, to zasób | ||
może dalej pozostać | może dalej pozostać niezwolniony. Rozwiązaniem może być kod: | ||
void f() { | |||
przydziel_zasob(); | |||
try { | |||
g(); /*może rzucić wyjątek*/ | |||
} | |||
cath(...) {zwolnij_zasob();throw;} | |||
zwolnij_zasob(); | |||
} | |||
Po zwolnieniu zasobu rzucamy (podrzucamy?) ponownie ten sam wyjątek. W | |||
ten sposób funkcja <code><nowiki> f()</nowiki></code> staje się "przeźroczysta dla wyjątków" | |||
(exception-neutral). | |||
ten sposób funkcja <code><nowiki>f()</nowiki></code> | |||
(exception | |||
==Konstruktory== | ==Konstruktory== | ||
Linia 73: | Linia 64: | ||
wyjątki rzucane z konstruktora. Rozważmy następujący kod: | wyjątki rzucane z konstruktora. Rozważmy następujący kod: | ||
struct BigRsource { | |||
char c[10000000]; | char c[10000000]; | ||
}; | }; | ||
struct BadBoy { | struct BadBoy { | ||
BadBoy() {throw 0;}; | BadBoy() {throw 0;}; | ||
}; | }; | ||
struct X { | struct X { | ||
BigResource *p1; | |||
BadBoy p2; | |||
X: p1(new BigResource) {} | X: p1(new BigResource) {} | ||
X() { | |||
delete p1; | |||
} | |||
} | |||
Na pierwszy rzut oka jest to pierwszorzędny przykład programowania | |||
Na pierwszy rzut oka jest to | |||
obiektowego: pamięć jest przydzielana w konstruktorze i zwalniana w | obiektowego: pamięć jest przydzielana w konstruktorze i zwalniana w | ||
destruktorze, nie ma więc możliwości wycieku. Prześledźmy jednak, co | destruktorze, nie ma więc możliwości wycieku. Prześledźmy jednak, co | ||
się stanie gdy napiszemy: | się stanie, gdy napiszemy: | ||
try { | |||
X x; | |||
} catch(...) {}; | |||
Konstruktor najpierw przydzieli pamięć dla wskaźnika <code><nowiki> p1</nowiki></code>. Załóżmy, | |||
Konstruktor najpierw przydzieli pamięć dla wskaźnika <code><nowiki>p1</nowiki></code>. Załóżmy, | |||
że ta alokacja się powiedzie. Następnie zostanie wywołany konstruktor | że ta alokacja się powiedzie. Następnie zostanie wywołany konstruktor | ||
<code><nowiki>BadBoy</nowiki></code>, który rzuci wyjątek. Wyjątek nie zostanie złapany w | <code><nowiki> BadBoy</nowiki></code>, który rzuci wyjątek. Wyjątek nie zostanie złapany w | ||
konstruktorze <code><nowiki>X</nowiki></code>, więc sterowanie zostane przekazane do klauzuli | konstruktorze <code><nowiki> X</nowiki></code>, więc sterowanie zostane przekazane do klauzuli | ||
<code><nowiki>catch</nowiki></code>. Nastąpi zwinięcie stosu, ale destruktor obiektu <code><nowiki>x</nowiki></code> | <code><nowiki> catch</nowiki></code>. Nastąpi zwinięcie stosu, ale destruktor obiektu <code><nowiki> x</nowiki></code> | ||
'''nie''' zostanie wywołany! Dzieje się tak dlatego, że w C++ | '''nie''' zostanie wywołany! Dzieje się tak dlatego, że w C++ | ||
destruktory nie są wołane dla obiektów, których konstrukcja się nie | destruktory nie są wołane dla obiektów, których konstrukcja się nie | ||
powiodła. W taki sposób tracimy 10MB. Możliwe rozwiązania są podobne | powiodła. W taki sposób tracimy 10MB. Możliwe rozwiązania są podobne | ||
jak w poprzednim wypadku: korzystamy z <code><nowiki>auto_ptr</nowiki></code>: | jak w poprzednim wypadku: korzystamy z <code><nowiki> auto_ptr</nowiki></code>: | ||
struct X { | |||
std::auto_ptr<BigResource> p1; | std::auto_ptr<BigResource> p1; | ||
BadBoy p2; | BadBoy p2; | ||
X: p1(new BigResource) {} | X: p1(new BigResource) {} | ||
X() { | X() { | ||
delete p1; | delete p1; | ||
} | } | ||
}; | }; | ||
lub sami łapiemy wyjątek: | lub sami łapiemy wyjątek: | ||
struct X { | |||
BigResource *p1; | BigResource *p1; | ||
BadBoy p2; | BadBoy p2; | ||
X: try {p1(new BigResource) {}} | X: try {p1(new BigResource) {}} | ||
catch(...){delete p1;}; | catch(...){delete p1;}; | ||
X() { | ~X() { | ||
delete p1; | delete p1; | ||
} | } | ||
} | } | ||
Proszę zwrócić uwagę na blok <code><nowiki> try</nowiki></code>, który otacza cały | |||
Proszę zwrócić uwagę na blok <code><nowiki>try</nowiki></code>, który otacza cały | |||
konstruktor łącznie z listą inicjalizatorów. | konstruktor łącznie z listą inicjalizatorów. | ||
==The bad, the good and the ugly== | ==The bad, the good and the ugly== | ||
Linia 145: | Linia 132: | ||
Wyróżnimy trzy możliwości: | Wyróżnimy trzy możliwości: | ||
Obiekt jest w stanie niekonsystentnym, nie | '''The bad. ''' Obiekt jest w stanie niekonsystentnym, nie | ||
są zachowane niezmienniki jego typu, być może nastąpił wyciek zasobów. | są zachowane niezmienniki jego typu, być może nastąpił wyciek zasobów. | ||
Nieokreślone jest zachowanie wywoływanych metod, w szczególności może | Nieokreślone jest zachowanie wywoływanych metod, w szczególności może nie powieść się destrukcja obiektu. | ||
Obiekt jest w stanie konsystentnym, ale | '''The ugly. ''' Obiekt jest w stanie konsystentnym, ale | ||
niezdefiniowanym. | niezdefiniowanym. | ||
Obiekt pozostaje w stanie, w jakim był przed | '''The good. ''' Obiekt pozostaje w stanie, w jakim był przed | ||
rzuceniem wyjątku. Jest to semantyka transakcji: commit--rollback. | rzuceniem wyjątku. Jest to semantyka transakcji: commit--rollback. | ||
Linia 159: | Linia 145: | ||
zawsze da się jednak zapewnić takie zachowanie bez ponoszenia dużych | zawsze da się jednak zapewnić takie zachowanie bez ponoszenia dużych | ||
kosztów. Wtedy możemy zadowolić się stanem drugim. Stan pierwszy | kosztów. Wtedy możemy zadowolić się stanem drugim. Stan pierwszy | ||
to oczywista katastrofa. | to oczywista katastrofa. | ||
==Przykład: stos== | ==Przykład: stos== | ||
Rozważmy stos z dynamiczną obsługą pamięci. Przykład takiego | Rozważmy stos z dynamiczną obsługą pamięci. Przykład takiego | ||
stosu był podany w | stosu był podany w [[Zaawansowane CPP/Wykład 7: Klasy wytycznych|Wykładzie 7.]] Żeby nie wprowadzać | ||
komplikacji, nie będziemy tu korzystać z klas wytycznych: | komplikacji, nie będziemy tu korzystać z klas wytycznych: | ||
template <class T,size_t N = 10> class Stack { | |||
size_t nelems; | |||
size_t top; | |||
T* v; | |||
public: | public: | ||
bool is_empty() const; | |||
void push(const T&); | |||
T pop(); | |||
Stack(size_t n = N); | |||
Stack(); | |||
Stack(const Stack&); | |||
Stack& operator=(const Stack&); | |||
}; | |||
W powyższym konstruktorze może nie powieść się tylko operacja | W powyższym konstruktorze może nie powieść się tylko operacja | ||
tworzenia tablicy <code><nowiki>v</nowiki></code>. Ale wtedy zgodnie z tym co już omawialiśmy w | tworzenia tablicy <code><nowiki> v</nowiki></code>. Ale wtedy, zgodnie z tym co już omawialiśmy w | ||
poprzednim wykładzie, wyrażenie <code><nowiki>new</nowiki></code> samo po sobie posprząta. Nie | poprzednim wykładzie, wyrażenie <code><nowiki> new</nowiki></code> samo po sobie posprząta. Nie | ||
musimy się martwić stanem pozostawionego obiektu, bo jeśli konstrukcja | |||
się nie powiedzie, to obiektu po prostu nie ma. | się nie powiedzie, to obiektu po prostu nie ma. | ||
Z konstruktorem kopiującym jest już trochę gorzej: | Z konstruktorem kopiującym jest już trochę gorzej: | ||
template <class T,size_t N> Stack<T,N>::Stack(const Stack<T,N>& s): | |||
v(new T[nelems | v(new T[nelems = s.nelems]) { | ||
if( s.top > 0 ) | if( s.top > 0 ) | ||
for(top | for(top = 0; top < s.top; top++) | ||
v[top] | v[top] = s.v[top]; /* tu może zostać rzucony wyjatek */ | ||
} | } | ||
Podobnie jak poprzednio, w wypadku niepowodzenia wyrażenie <code><nowiki> new</nowiki></code> | |||
Podobnie jak poprzednio, w wypadku niepowodzenia wyrażenie <code><nowiki>new</nowiki></code> | posprząta po sobie. Ale wyjątek może zostać rzucony również przez | ||
posprząta po sobie. Ale wyjątek może zostać rzucony również | operator przypisania klasy <code><nowiki> T</nowiki></code>. Wtedy będziemy mieli do czynienia z | ||
operator przypisania klasy <code><nowiki>T</nowiki></code>. Wtedy będziemy mieli do czynienia z | |||
wyciekiem pamięci, ponieważ nie zostanie wywołany destruktor stosu, | wyciekiem pamięci, ponieważ nie zostanie wywołany destruktor stosu, | ||
który zwalnia pamięć <code><nowiki>v</nowiki></code>. Taki przykład już omawialiśmy na początku | który zwalnia pamięć <code><nowiki> v</nowiki></code>. Taki przykład już omawialiśmy na początku | ||
wykładu. Rozwiązaniem jest użycie <code><nowiki>auto_ptr</nowiki></code> lub przechwycenie wyjątku: | wykładu. Rozwiązaniem jest użycie <code><nowiki> auto_ptr</nowiki></code> lub przechwycenie wyjątku: | ||
template <class T,size_t N> Stack<T,N>::Stack(const Stack<T,N>& s): | |||
v(new T[nelems | v(new T[nelems = s.nelems]) { | ||
try { | try { | ||
if( s.top > 0 ) | if( s.top > 0 ) | ||
for(top | for(top = 0; top < s.top; top++) | ||
v[top] | v[top] = s.v[top]; /* tu może zostać rzucony wyjatek */ | ||
} | } | ||
catch(...) { | catch(...) { | ||
delete [] v; throw ; | delete [] v; throw ; | ||
} | } | ||
} | } | ||
To rozwiązanie zakłada, że destrukcja <code><nowiki> v</nowiki></code> powiedzie się, tzn. że operator | |||
To rozwiązanie zakłada, że destrukcja <code><nowiki>v</nowiki></code> powiedzie się, tzn. | |||
przypisania: | przypisania: | ||
v[top] = s.v[top]; | |||
pozostawił lewą stronę w stanie umożliwiającym jej destrukcję. | pozostawił lewą stronę w stanie umożliwiającym jej destrukcję. | ||
Sytuacja jest groźniejsza w przypadku operatora przypisania: | Sytuacja jest groźniejsza w przypadku operatora przypisania: | ||
template <class T,size_t N> Stack<T,N>& | |||
Stack<T,N>::operator | Stack<T,N>::operator=(const Stack<T,N>& s) { | ||
delete [ ] v; | delete [ ] v; | ||
v | v = new T[nelems=s.nelems]; | ||
if( s.top > 0 ) | if( s.top > 0 ) | ||
for(top | for(top = 0; top < s.top; top++) | ||
v[top] | v[top] = s.v[top]; | ||
return *this; | return *this; | ||
} | } | ||
Wyjątek rzucony przez wyrażenie <code><nowiki> new</nowiki></code> zostawia stos w stanie złym, | |||
Wyjątek rzucony przez wyrażenie <code><nowiki>new</nowiki></code> zostawia stos w stanie złym | z wiszącym luźno wskaźnikiem <code><nowiki> v</nowiki></code>. Wyjątek rzucony przez operator | ||
z wiszącym luźno wskaźnikiem <code><nowiki>v</nowiki></code>. Wyjątek rzucony przez operator | przypisania elementów tablicy <code><nowiki> v</nowiki></code> w najlepszym przypadku zostawia | ||
przypisania elementów tablicy <code><nowiki>v</nowiki></code> w najlepszym przypadku zostawia | |||
stos w stanie niezdefiniowanym. Implementacja, która w wypadku | stos w stanie niezdefiniowanym. Implementacja, która w wypadku | ||
wystąpienia wyjątku zostawia stos | wystąpienia wyjątku zostawia stos w takim stanie, w jakim go zastała, | ||
jest podana poniżej: | jest podana poniżej: | ||
template <class T,size_t N> Stack<T,N>& | |||
Stack<T,N>::operator | Stack<T,N>::operator=(const Stack<T,N>& s) { | ||
T *tmp; | T *tmp; | ||
try { | try { | ||
tmp | tmp = new T[nelems=s.nelems]; | ||
if( s.top > 0 ) | if( s.top > 0 ) | ||
for(size_t i | for(size_t i = 0; i < s.top; i++) | ||
tmp[i] | tmp[i] = s.v[i]; | ||
} | } | ||
catch(...) {delete [] tmp,throw;} | catch(...) {delete [] tmp,throw;} | ||
swap(v,tmp); | swap(v,tmp); | ||
delete [] tmp; | delete [] tmp; | ||
top | top=s.top; | ||
return *this; | return *this; | ||
} | } | ||
Przejdźmy teraz do podstawowych funkcji stosu, zaczynając od funkcji <code><nowiki> push</nowiki></code>: | |||
Przejdźmy teraz do podstawowych funkcji stosu, zaczynając od funkcji <code><nowiki>push</nowiki></code>: | |||
template <class T,size_t N> | |||
void Stack<T,N>::push(const T &element) { | void Stack<T,N>::push(const T &element) { | ||
if( top | if( top == nelems ) { | ||
T* new_buffer | T* new_buffer = new T[nelems += N]; | ||
for(int i | for(int i = 0; i < top; i++) | ||
new_buffer[i] | new_buffer[i] = v[i]; | ||
delete [] v; | delete [] v; | ||
v | v = new_buffer; | ||
} | } | ||
v[top++] = element; | |||
v[top++] | } | ||
} | |||
Załóżmy na początek, że nie ma potrzeby zwiększania pamięci, | Załóżmy na początek, że nie ma potrzeby zwiększania pamięci, | ||
wykonywane jest więc tylko polecenie: | wykonywane jest więc tylko polecenie: | ||
v[top++] = element; | |||
Jak już zauważyliśmy, przypisanie może się nie powieść, wtedy stos | Jak już zauważyliśmy, przypisanie może się nie powieść, wtedy stos | ||
zostanie w stanie złym lub niezdefiniowanym, ponieważ <code><nowiki>top</nowiki></code> zostanie | zostanie w stanie złym lub niezdefiniowanym, ponieważ <code><nowiki> top</nowiki></code> zostanie | ||
zwiększone. Lepiej jest więc napisać: | zwiększone. Lepiej jest więc napisać: | ||
v[top] = element; | |||
++top; | ++top; | ||
Zobaczmy, co się dzieje, jeśli zażądamy zwiększenia pamięci. | Zobaczmy, co się dzieje, jeśli zażądamy zwiększenia pamięci. | ||
Niepowodzenie wyrażenia <code><nowiki>new</nowiki></code> zostawi nas ze zwiększonym polem | Niepowodzenie wyrażenia <code><nowiki> new</nowiki></code> zostawi nas ze zwiększonym polem | ||
<code><nowiki>nelems</nowiki></code> pomimo | <code><nowiki> nelems</nowiki></code>, pomimo że pamięć się nie zwiększyła. Wyjątek z operatora | ||
przypisania zostawi nas z wyciekiem pamięci, ponieważ pamięć | przypisania zostawi nas z wyciekiem pamięci, ponieważ pamięć | ||
przydzielona do <code><nowiki> new_buffer</nowiki></code> nigdy nie zostanie zwolniona. | |||
Uwzględaniając te uwagi, poprawimy funkcję <code><nowiki>push</nowiki></code> następująco: | Uwzględaniając te uwagi, poprawimy funkcję <code><nowiki> push</nowiki></code> następująco: | ||
template <class T,size_t N> | |||
void Stack<T,N>::push(T element) { | void Stack<T,N>::push(T element) { | ||
if( top | if( top == nelems ) { | ||
T* new_buffer; | T* new_buffer; | ||
size_t new_nelems; | size_t new_nelems; | ||
try { | try { | ||
new_nelems | new_nelems=nelems+N; | ||
new_buffer | new_buffer = new T[new_nelems]; | ||
for(int i | for(int i = 0; i < top; i++) | ||
new_buffer[i] | new_buffer[i] = v[i]; | ||
} | } | ||
catch(...) { delete [] new_buffer;} | catch(...) { delete [] new_buffer;} | ||
swap(v,new_buffer); | swap(v,new_buffer); | ||
delete [] new_buffer; | delete [] new_buffer; | ||
nelems | nelems = new_nelems; | ||
} | } | ||
v[top] | v[top] = element; | ||
++top; | ++top; | ||
} | } | ||
Na koniec została nam jeszcze funkcja <code><nowiki> pop</nowiki></code>: | |||
if( top | |||
template <class T,size_t N> T Stack<T,N>::pop() { | |||
if( top == 0 ) | |||
throw std::domain_error("pop on empty stack"); | throw std::domain_error("pop on empty stack"); | ||
return v[--top]; /* tu może nastąpić kopiowanie */ | return v[--top]; /* tu może nastąpić kopiowanie */ | ||
} | } | ||
Jak widać funkcja <code><nowiki> pop</nowiki></code> może rzucić jawnie wyjątek | |||
Jak widać funkcja <code><nowiki>pop</nowiki></code> może rzucić jawnie wyjątek | <code><nowiki> std::domain_error</nowiki></code>. Z tym wyjątkiem nie ma problemów. | ||
<code><nowiki>std::domain_error</nowiki></code>. Z tym wyjątkiem nie ma problemów. | |||
Potencjalny problem stwarza za to wyrażenie: | Potencjalny problem stwarza za to wyrażenie: | ||
return v[--top]; /* tu może nastąpić kopiowanie */ | |||
Ponieważ zwracamy <code><nowiki> v[--top]</nowiki></code> przez wartość, to może nastąpić | |||
Ponieważ zwracamy <code><nowiki>v[--top]</nowiki></code> przez wartość, to może nastąpić | kopiowanie elementu typu <code><nowiki> T</nowiki></code>. Nie musi, ponieważ kompilator ma prawo | ||
kopiowanie | |||
wyoptymalizować powstały obiekt tymczasowy. Jeżeli jednak zostanie | wyoptymalizować powstały obiekt tymczasowy. Jeżeli jednak zostanie | ||
wywołany konstruktor kopiujący, to może rzucić wyjątek. Wtedy stos | wywołany konstruktor kopiujący, to może rzucić wyjątek. Wtedy stos | ||
pozostanie w zmienionym stanie, bo wartość <code><nowiki>top</nowiki></code> zostanie zmniejszona. | pozostanie w zmienionym stanie, bo wartość <code><nowiki> top</nowiki></code> zostanie zmniejszona. | ||
Rozważmy też wyrażenie: | Rozważmy też wyrażenie: | ||
x = s.pop(); | |||
Jeżeli operacja przypisania się nie powiedzie, to stracimy | Jeżeli operacja przypisania się nie powiedzie, to stracimy | ||
bezpowrotnie jeden element stosu. Można by powiedzieć, że to już nie | bezpowrotnie jeden element stosu. Można by powiedzieć, że to już nie | ||
jest sprawa stosu, ale lepiej po | jest sprawa stosu, ale lepiej po prostu rozdzielić operacje | ||
modyfikujące stan stosu od operacji tylko ten stan odczytujących: | modyfikujące stan stosu od operacji tylko ten stan odczytujących: | ||
template <class T,size_t N> void Stack<T,N>::pop() { | |||
if( top | if( top == 0 ) | ||
throw std::domain_error("pop on empty stack"); | throw std::domain_error("pop on empty stack"); | ||
--top; | --top; | ||
} | } | ||
template<class T,size_t N> T &Stack<T,N>::top() { | template<class T,size_t N> T &Stack<T,N>::top() { | ||
if( top | if( top == 0 ) | ||
throw std::domain_error("pop on empty stack"); | throw std::domain_error("pop on empty stack"); | ||
return v[top-1]; | return v[top-1]; | ||
} | } | ||
template<class T,size_t N> const T &Stack<T,N>::top() const { | template<class T,size_t N> const T &Stack<T,N>::top() const { | ||
if( top | if( top == 0 ) | ||
throw std::domain_error("pop on empty stack"); | throw std::domain_error("pop on empty stack"); | ||
return v[top-1]; | |||
} | |||
W przeciwieństwie do <code><nowiki> pop()</nowiki></code> operacja <code><nowiki> top()</nowiki></code> może zwracać | |||
wartość przez referencje. Funkcja <code><nowiki> pop()</nowiki></code> robić tego w ogólności | |||
nie mogła, bo potencjalnie niszczyła obiekt zdejmowany ze stosu. | |||
W przeciwieństwie do <code><nowiki>pop()</nowiki></code> operacja <code><nowiki>top()</nowiki></code> może zwracać | |||
wartość przez referencje. Funkcja <code><nowiki>pop()</nowiki></code> robić tego w ogólności | |||
nie mogła, bo potencjalnie niszczyła obiekt zdejmowany ze stosu. | |||
==Kolejny stos== | ==Kolejny stos== | ||
Zaprezentowana w poprzedniej części implementacja stosu wymagała, aby | Zaprezentowana w poprzedniej części implementacja stosu wymagała, aby | ||
parametr szablonu <code><nowiki>T</nowiki></code> posiadał: | parametr szablonu <code><nowiki> T</nowiki></code> posiadał: | ||
* | * konstruktor domyślny. | ||
* | * bezpieczny (względem wyjątków) operator przypisania. | ||
* | * destruktor nie rzucający wyjątków. | ||
Proszę zauważyć, że konstruktor domyślny właściwie niczemu nie służy. | Proszę zauważyć, że konstruktor domyślny właściwie niczemu nie służy. | ||
Linia 399: | Linia 362: | ||
inicjalizacja i przypisanie jest w C++ dokonywana za pomocą | inicjalizacja i przypisanie jest w C++ dokonywana za pomocą | ||
konstruktora kopiującego. Na zakończenie przedstawię implementację | konstruktora kopiującego. Na zakończenie przedstawię implementację | ||
klasy <code><nowiki>Stack</nowiki></code>, która od typu <code><nowiki>T</nowiki></code> potrzebuje tylko destruktora i | klasy <code><nowiki> Stack</nowiki></code>, która od typu <code><nowiki> T</nowiki></code> potrzebuje tylko destruktora i | ||
konstruktora kopiującego. W tym celu będziemy przydzielać "gołą" | konstruktora kopiującego. W tym celu będziemy przydzielać "gołą" | ||
pamięć oraz tworzyć i niszczyć w niej obiekty bezpośrednio. Do tego celu | pamięć oraz tworzyć i niszczyć w niej obiekty bezpośrednio. Do tego celu | ||
Linia 405: | Linia 368: | ||
Zaczniemy od zdefiniowania pomocniczej klasy do zarządzania pamięcią: | Zaczniemy od zdefiniowania pomocniczej klasy do zarządzania pamięcią: | ||
template<typename T,typename Allocator = std::allocator<T> > | |||
struct Stack_impl : public Allocator{ | struct Stack_impl : public Allocator{ | ||
size_t _top; | size_t _top; | ||
size_t _size; | size_t _size; | ||
T* _buffer; | T* _buffer; | ||
Stack_impl(size_t n): | Stack_impl(size_t n): | ||
_top(0), | _top(0), | ||
Linia 417: | Linia 380: | ||
Stack_impl() { | Stack_impl() { | ||
for(size_t i | for(size_t i=0;i<_top;++i) | ||
destroy(_buffer++); | destroy(_buffer++); | ||
deallocate(_buffer,_size); | deallocate(_buffer,_size); | ||
} | } | ||
void swap(Stack_impl& rhs) throw() { | void swap(Stack_impl& rhs) throw() { | ||
std::swap(_buffer,rhs._buffer); | std::swap(_buffer,rhs._buffer); | ||
Linia 428: | Linia 391: | ||
std::swap(_top,rhs._top); | std::swap(_top,rhs._top); | ||
} | } | ||
}; | }; | ||
Jedyne miejsce, gdzie może zostać rzucony wyjątek to funkcja | Jedyne miejsce, gdzie może zostać rzucony wyjątek to funkcja | ||
<code><nowiki>allocate()</nowiki></code>, ale wtedy żadna pamięć nie zostanie przydzielona ani | <code><nowiki> allocate()</nowiki></code>, ale wtedy żadna pamięć nie zostanie przydzielona ani | ||
żaden obiekt nie zostanie stworzony. Korzystamy tu też z żądania, aby | żaden obiekt nie zostanie stworzony. Korzystamy tu też z żądania, aby | ||
alokator był bezstanowy, inaczej funkcja <code><nowiki>swap</nowiki></code> musiałaby też | alokator był bezstanowy, inaczej funkcja <code><nowiki> swap</nowiki></code> musiałaby też | ||
zamieniać składowe alokatorów. | zamieniać składowe alokatorów. | ||
Klasa <code><nowiki>Stack</nowiki></code> korzysta z klasy <code><nowiki>Stack_impl</nowiki></code>: | Klasa <code><nowiki> Stack</nowiki></code> korzysta z klasy <code><nowiki> Stack_impl</nowiki></code>: | ||
template<typename T,size_t N = 10, | |||
typename Allocator = std::allocator<T> > | |||
class Stack { | |||
private: | |||
Stack_impl<T,Allocator> _impl; | |||
([[media:Stack_sutter.h | Źródło: stack_sutter.h]]) | |||
Konstruktory: | Konstruktory: | ||
public: | |||
Stack(size_t n = N):_impl(n) {}; | |||
Stack(const Stack& rhs):_impl(rhs._impl) { | Stack(const Stack& rhs):_impl(rhs._impl) { | ||
while(_impl._top < rhs._impl._top) { | while(_impl._top < rhs._impl._top) { | ||
Linia 458: | Linia 419: | ||
} | } | ||
} | } | ||
([[media:Stack_sutter.h | Źródło: stack_sutter.h]]) | |||
robią się teraz prostsze. Nie ma potrzeby definiowania destruktora. | robią się teraz prostsze. Nie ma potrzeby definiowania destruktora. | ||
Destruktor domyślny sam wywoła destruktor pola <code><nowiki>_impl</nowiki></code>. Jeżeli w | Destruktor domyślny sam wywoła destruktor pola <code><nowiki> _impl</nowiki></code>. Jeżeli w | ||
konstruktorze kopiującym | konstruktorze kopiującym zostanie rzucony wyjątek w funkcji | ||
<code><nowiki>construct</nowiki></code>, to wywołany podczas zwijania stosu destruktor <code><nowiki>Stack_impl</nowiki></code> | <code><nowiki> construct</nowiki></code>, to wywołany podczas zwijania stosu destruktor <code><nowiki> Stack_impl</nowiki></code> | ||
wywoła destruktory stworzonych obiektów i zwolni pamięć. | wywoła destruktory stworzonych obiektów i zwolni pamięć. | ||
Operator przypisania korzysta z "triku": | Operator przypisania korzysta z "triku": | ||
Stack &operator=(const Stack& rhs) { | |||
Stack tmp(rhs); | Stack tmp(rhs); | ||
_impl.swap(tmp._impl); | _impl.swap(tmp._impl); | ||
return *this; | return *this; | ||
} | } | ||
([[media:Stack_sutter.h | Źródło: stack_sutter.h]]) | |||
Tworzymy kopię prawej strony i zamieniamy z lewą stroną. Obiekt | |||
Tworzymy | <code><nowiki> tmp</nowiki></code> jest obiektem lokalnym, więc zostanie zniszczony. Jeśli nie | ||
<code><nowiki>tmp</nowiki></code> jest obiektem lokalnym, więc zostanie zniszczony. Jeśli nie | |||
powiedzie się kopiowanie, to stos pozostaje w stanie niezmienionym. | powiedzie się kopiowanie, to stos pozostaje w stanie niezmienionym. | ||
Proszę zauważyć, że jest to bezpieczne nawet w przypadku | Proszę zauważyć, że jest to bezpieczne nawet w przypadku | ||
samopodstawienia <code><nowiki>s | samopodstawienia <code><nowiki> s=s</nowiki></code>. | ||
Funkcja <code><nowiki>push</nowiki></code> stosuje podobną technikę: | Funkcja <code><nowiki> push</nowiki></code> stosuje podobną technikę: | ||
void push(const T &elem) { | |||
if(_impl._top==_impl._size) { | |||
Stack tmp(_impl._size+N); | Stack tmp(_impl._size+N); | ||
while(tmp._impl._top < _impl._top) { | while(tmp._impl._top < _impl._top) { | ||
_impl.construct(tmp._impl._buffer+tmp._impl._top, | |||
_impl._buffer[tmp._impl._top]); | |||
++tmp._impl._top; | |||
} | |||
_impl.swap(tmp._impl); | _impl.swap(tmp._impl); | ||
} | } | ||
Linia 500: | Linia 459: | ||
++_impl._top; | ++_impl._top; | ||
} | } | ||
([[media:Stack_sutter.h | Źródło: stack_sutter.h]]) | |||
Funkcje <code><nowiki> top()</nowiki></code> i <code><nowiki> pop()</nowiki></code> pozostają praktycznie niezmienione, z | |||
tym, że funkcja <code><nowiki> pop()</nowiki></code> niszczy obiekt na wierzchołku stosu: | |||
T &top() { | |||
if(_impl._top==0) | |||
throw std::domain_error("empty stack"); | throw std::domain_error("empty stack"); | ||
return _impl._buffer[_impl._top-1]; | return _impl._buffer[_impl._top-1]; | ||
} | } | ||
void pop() { | void pop() { | ||
if(_impl._top | if(_impl._top==0) | ||
throw std::domain_error("empty stack"); | throw std::domain_error("empty stack"); | ||
Linia 519: | Linia 477: | ||
_impl.destroy(_impl._buffer+_impl._top); | _impl.destroy(_impl._buffer+_impl._top); | ||
} | } | ||
bool is_empty() { | bool is_empty() { | ||
return _impl._top | return _impl._top==0; | ||
} | } | ||
}; | |||
}; | ([[media:Stack_sutter.h | Źródło: stack_sutter.h]]) | ||
Aktualna wersja na dzień 22:02, 4 paź 2006
Wstęp
W poprzednim wykładzie opisałem mechanizm obsługi błędów za pomocą
wyjątków. Jest to bardzo silny mechanizm: rzucony wyjątek powoduje
natychmiastowe przekazanie sterowania do najbliższej klauzuli
catch
, niejako "tnąc" w poprzek dowolnie głęboko zagnieżdżone
funkcje. To oczywiście jest jedną z jego podstawowych zalet, ale musimy
podchodzić do tej własności bardzo ostrożnie.
W tym wykładzie zwrócę uwagę na kilka niebezpieczeństw wynikających z obsługi wyjątków i na sposoby zapobiegania im.
Wyjątkowe niebezpieczeństwa
W zasadzie korzystanie z wyjątków jest proste: funkcja, która stwierdza wystąpienie błędu, a nie umie go sama obsłużyć, przekazuje odpowiedzialność swoim przełożonym, rzucając wyjątek. Jej przełożeni mogą zrobić to samo (wystarczy, że nie przechwycą wyjątku). Zakładamy jednak, że gdzieś w tej hierarchii wyjątek zostanie złapany przez kogoś, kto wie jak go obsłużyć. W praktyce sprawa może być bardziej skomplikowana. Rzucony wyjątek powoduje natychmiastowe przerwanie nie tylko funkcji, która go rzuciła, ale również wszystkich funkcji, przez które "przelatuje". Jeśli te funkcje nie są na to przygotowane, to wyjątek może narobić dodatkowych szkód. Typowy przykład to niezwolnione zasoby:
void f() { przydziel_zasob(); g(); /*może rzucić wyjątek*/ zwolnij_zasob(); }
Rzucenie wyjątku z g()
spowoduje wyciek zasobu (zwykle pamięci). Taki przykład był już rozważany w Wykładzie 10
Podane tam rozwiązanie to technika "przydział zasobu jest
inicjalizacją", czyli oddelegowanie zarządzania zasobem do osobnej
klasy, której konstruktor przydziela zasób, a destruktor zwalnia:
void f() { Zasob x; g(); /*może rzucić wyjątek*/ } /* niejawnie wywoływany destruktor x. Zasob() */
Wtedy podczas zwijania stosu zasób zostanie zwolniony automatycznie. Proszę zauważyć jednak, że jeśli nie przechwycimy wyjątku, to zasób może dalej pozostać niezwolniony. Rozwiązaniem może być kod:
void f() { przydziel_zasob(); try { g(); /*może rzucić wyjątek*/ } cath(...) {zwolnij_zasob();throw;} zwolnij_zasob(); }
Po zwolnieniu zasobu rzucamy (podrzucamy?) ponownie ten sam wyjątek. W
ten sposób funkcja f()
staje się "przeźroczysta dla wyjątków"
(exception-neutral).
Konstruktory
Szczególnym przypadkiem mogącym prowadzić do wycieku pamięci są wyjątki rzucane z konstruktora. Rozważmy następujący kod:
struct BigRsource { char c[10000000]; }; struct BadBoy { BadBoy() {throw 0;}; }; struct X { BigResource *p1; BadBoy p2; X: p1(new BigResource) {} X() { delete p1; } }
Na pierwszy rzut oka jest to pierwszorzędny przykład programowania obiektowego: pamięć jest przydzielana w konstruktorze i zwalniana w destruktorze, nie ma więc możliwości wycieku. Prześledźmy jednak, co się stanie, gdy napiszemy:
try { X x; } catch(...) {};
Konstruktor najpierw przydzieli pamięć dla wskaźnika p1
. Załóżmy,
że ta alokacja się powiedzie. Następnie zostanie wywołany konstruktor
BadBoy
, który rzuci wyjątek. Wyjątek nie zostanie złapany w
konstruktorze X
, więc sterowanie zostane przekazane do klauzuli
catch
. Nastąpi zwinięcie stosu, ale destruktor obiektu x
nie zostanie wywołany! Dzieje się tak dlatego, że w C++
destruktory nie są wołane dla obiektów, których konstrukcja się nie
powiodła. W taki sposób tracimy 10MB. Możliwe rozwiązania są podobne
jak w poprzednim wypadku: korzystamy z auto_ptr
:
struct X { std::auto_ptr<BigResource> p1; BadBoy p2; X: p1(new BigResource) {} X() { delete p1; } };
lub sami łapiemy wyjątek:
struct X { BigResource *p1; BadBoy p2; X: try {p1(new BigResource) {}} catch(...){delete p1;}; ~X() { delete p1; } }
Proszę zwrócić uwagę na blok try
, który otacza cały
konstruktor łącznie z listą inicjalizatorów.
The bad, the good and the ugly
Jeżeli wyjątek został rzucony przez metodę jakiegoś obiektu, to dla dalszego działania programu ważne jest, w jakim stanie go pozostawił. Wyróżnimy trzy możliwości:
The bad. Obiekt jest w stanie niekonsystentnym, nie są zachowane niezmienniki jego typu, być może nastąpił wyciek zasobów. Nieokreślone jest zachowanie wywoływanych metod, w szczególności może nie powieść się destrukcja obiektu.
The ugly. Obiekt jest w stanie konsystentnym, ale niezdefiniowanym.
The good. Obiekt pozostaje w stanie, w jakim był przed rzuceniem wyjątku. Jest to semantyka transakcji: commit--rollback.
Ewidentnie najbardziej pożądanym zachowaniem jest stan ostatni. Nie zawsze da się jednak zapewnić takie zachowanie bez ponoszenia dużych kosztów. Wtedy możemy zadowolić się stanem drugim. Stan pierwszy to oczywista katastrofa.
Przykład: stos
Rozważmy stos z dynamiczną obsługą pamięci. Przykład takiego stosu był podany w Wykładzie 7. Żeby nie wprowadzać komplikacji, nie będziemy tu korzystać z klas wytycznych:
template <class T,size_t N = 10> class Stack { size_t nelems; size_t top; T* v; public: bool is_empty() const; void push(const T&); T pop(); Stack(size_t n = N); Stack(); Stack(const Stack&); Stack& operator=(const Stack&); };
W powyższym konstruktorze może nie powieść się tylko operacja
tworzenia tablicy v
. Ale wtedy, zgodnie z tym co już omawialiśmy w
poprzednim wykładzie, wyrażenie new
samo po sobie posprząta. Nie
musimy się martwić stanem pozostawionego obiektu, bo jeśli konstrukcja
się nie powiedzie, to obiektu po prostu nie ma.
Z konstruktorem kopiującym jest już trochę gorzej:
template <class T,size_t N> Stack<T,N>::Stack(const Stack<T,N>& s): v(new T[nelems = s.nelems]) { if( s.top > 0 ) for(top = 0; top < s.top; top++) v[top] = s.v[top]; /* tu może zostać rzucony wyjatek */ }
Podobnie jak poprzednio, w wypadku niepowodzenia wyrażenie new
posprząta po sobie. Ale wyjątek może zostać rzucony również przez
operator przypisania klasy T
. Wtedy będziemy mieli do czynienia z
wyciekiem pamięci, ponieważ nie zostanie wywołany destruktor stosu,
który zwalnia pamięć v
. Taki przykład już omawialiśmy na początku
wykładu. Rozwiązaniem jest użycie auto_ptr
lub przechwycenie wyjątku:
template <class T,size_t N> Stack<T,N>::Stack(const Stack<T,N>& s): v(new T[nelems = s.nelems]) { try { if( s.top > 0 ) for(top = 0; top < s.top; top++) v[top] = s.v[top]; /* tu może zostać rzucony wyjatek */ } catch(...) { delete [] v; throw ; } }
To rozwiązanie zakłada, że destrukcja v
powiedzie się, tzn. że operator
przypisania:
v[top] = s.v[top];
pozostawił lewą stronę w stanie umożliwiającym jej destrukcję.
Sytuacja jest groźniejsza w przypadku operatora przypisania:
template <class T,size_t N> Stack<T,N>& Stack<T,N>::operator=(const Stack<T,N>& s) { delete [ ] v; v = new T[nelems=s.nelems]; if( s.top > 0 ) for(top = 0; top < s.top; top++) v[top] = s.v[top]; return *this; }
Wyjątek rzucony przez wyrażenie new
zostawia stos w stanie złym,
z wiszącym luźno wskaźnikiem v
. Wyjątek rzucony przez operator
przypisania elementów tablicy v
w najlepszym przypadku zostawia
stos w stanie niezdefiniowanym. Implementacja, która w wypadku
wystąpienia wyjątku zostawia stos w takim stanie, w jakim go zastała,
jest podana poniżej:
template <class T,size_t N> Stack<T,N>& Stack<T,N>::operator=(const Stack<T,N>& s) { T *tmp; try { tmp = new T[nelems=s.nelems]; if( s.top > 0 ) for(size_t i = 0; i < s.top; i++) tmp[i] = s.v[i]; } catch(...) {delete [] tmp,throw;} swap(v,tmp); delete [] tmp; top=s.top; return *this; }
Przejdźmy teraz do podstawowych funkcji stosu, zaczynając od funkcji push
:
template <class T,size_t N> void Stack<T,N>::push(const T &element) { if( top == nelems ) { T* new_buffer = new T[nelems += N]; for(int i = 0; i < top; i++) new_buffer[i] = v[i]; delete [] v; v = new_buffer; } v[top++] = element; }
Załóżmy na początek, że nie ma potrzeby zwiększania pamięci, wykonywane jest więc tylko polecenie:
v[top++] = element;
Jak już zauważyliśmy, przypisanie może się nie powieść, wtedy stos
zostanie w stanie złym lub niezdefiniowanym, ponieważ top
zostanie
zwiększone. Lepiej jest więc napisać:
v[top] = element; ++top;
Zobaczmy, co się dzieje, jeśli zażądamy zwiększenia pamięci.
Niepowodzenie wyrażenia new
zostawi nas ze zwiększonym polem
nelems
, pomimo że pamięć się nie zwiększyła. Wyjątek z operatora
przypisania zostawi nas z wyciekiem pamięci, ponieważ pamięć
przydzielona do new_buffer
nigdy nie zostanie zwolniona.
Uwzględaniając te uwagi, poprawimy funkcję push
następująco:
template <class T,size_t N> void Stack<T,N>::push(T element) { if( top == nelems ) { T* new_buffer; size_t new_nelems; try { new_nelems=nelems+N; new_buffer = new T[new_nelems]; for(int i = 0; i < top; i++) new_buffer[i] = v[i]; } catch(...) { delete [] new_buffer;} swap(v,new_buffer); delete [] new_buffer; nelems = new_nelems; } v[top] = element; ++top; }
Na koniec została nam jeszcze funkcja pop
:
template <class T,size_t N> T Stack<T,N>::pop() { if( top == 0 ) throw std::domain_error("pop on empty stack"); return v[--top]; /* tu może nastąpić kopiowanie */ }
Jak widać funkcja pop
może rzucić jawnie wyjątek
std::domain_error
. Z tym wyjątkiem nie ma problemów.
Potencjalny problem stwarza za to wyrażenie:
return v[--top]; /* tu może nastąpić kopiowanie */
Ponieważ zwracamy v[--top]
przez wartość, to może nastąpić
kopiowanie elementu typu T
. Nie musi, ponieważ kompilator ma prawo
wyoptymalizować powstały obiekt tymczasowy. Jeżeli jednak zostanie
wywołany konstruktor kopiujący, to może rzucić wyjątek. Wtedy stos
pozostanie w zmienionym stanie, bo wartość top
zostanie zmniejszona.
Rozważmy też wyrażenie:
x = s.pop();
Jeżeli operacja przypisania się nie powiedzie, to stracimy bezpowrotnie jeden element stosu. Można by powiedzieć, że to już nie jest sprawa stosu, ale lepiej po prostu rozdzielić operacje modyfikujące stan stosu od operacji tylko ten stan odczytujących:
template <class T,size_t N> void Stack<T,N>::pop() { if( top == 0 ) throw std::domain_error("pop on empty stack"); --top; } template<class T,size_t N> T &Stack<T,N>::top() { if( top == 0 ) throw std::domain_error("pop on empty stack"); return v[top-1]; } template<class T,size_t N> const T &Stack<T,N>::top() const { if( top == 0 ) throw std::domain_error("pop on empty stack"); return v[top-1]; }
W przeciwieństwie do pop()
operacja top()
może zwracać
wartość przez referencje. Funkcja pop()
robić tego w ogólności
nie mogła, bo potencjalnie niszczyła obiekt zdejmowany ze stosu.
Kolejny stos
Zaprezentowana w poprzedniej części implementacja stosu wymagała, aby
parametr szablonu T
posiadał:
- konstruktor domyślny.
- bezpieczny (względem wyjątków) operator przypisania.
- destruktor nie rzucający wyjątków.
Proszę zauważyć, że konstruktor domyślny właściwie niczemu nie służy.
Jest potrzebny tylko po to, aby stworzyć tablicę obiektów, które potem
będą tak naprawdę nadpisywane za pomocą operatora przypisania. Taka
inicjalizacja i przypisanie jest w C++ dokonywana za pomocą
konstruktora kopiującego. Na zakończenie przedstawię implementację
klasy Stack
, która od typu T
potrzebuje tylko destruktora i
konstruktora kopiującego. W tym celu będziemy przydzielać "gołą"
pamięć oraz tworzyć i niszczyć w niej obiekty bezpośrednio. Do tego celu
wykorzystamy alokator opisany w poprzednim wykładzie.
Zaczniemy od zdefiniowania pomocniczej klasy do zarządzania pamięcią:
template<typename T,typename Allocator = std::allocator<T> > struct Stack_impl : public Allocator{ size_t _top; size_t _size; T* _buffer; Stack_impl(size_t n): _top(0), _size(n), _buffer(Allocator::allocate(_size)) {}; Stack_impl() { for(size_t i=0;i<_top;++i) destroy(_buffer++); deallocate(_buffer,_size); } void swap(Stack_impl& rhs) throw() { std::swap(_buffer,rhs._buffer); std::swap(_size,rhs._size); std::swap(_top,rhs._top); } };
Jedyne miejsce, gdzie może zostać rzucony wyjątek to funkcja
allocate()
, ale wtedy żadna pamięć nie zostanie przydzielona ani
żaden obiekt nie zostanie stworzony. Korzystamy tu też z żądania, aby
alokator był bezstanowy, inaczej funkcja swap
musiałaby też
zamieniać składowe alokatorów.
Klasa Stack
korzysta z klasy Stack_impl
:
template<typename T,size_t N = 10, typename Allocator = std::allocator<T> > class Stack { private: Stack_impl<T,Allocator> _impl;
Konstruktory:
public: Stack(size_t n = N):_impl(n) {}; Stack(const Stack& rhs):_impl(rhs._impl) { while(_impl._top < rhs._impl._top) { _impl.construct(_impl._buffer+_impl._top, rhs._impl._buffer[_impl._top]); ++_impl._top; } }
robią się teraz prostsze. Nie ma potrzeby definiowania destruktora.
Destruktor domyślny sam wywoła destruktor pola _impl
. Jeżeli w
konstruktorze kopiującym zostanie rzucony wyjątek w funkcji
construct
, to wywołany podczas zwijania stosu destruktor Stack_impl
wywoła destruktory stworzonych obiektów i zwolni pamięć.
Operator przypisania korzysta z "triku":
Stack &operator=(const Stack& rhs) { Stack tmp(rhs); _impl.swap(tmp._impl); return *this; }
Tworzymy kopię prawej strony i zamieniamy z lewą stroną. Obiekt
tmp
jest obiektem lokalnym, więc zostanie zniszczony. Jeśli nie
powiedzie się kopiowanie, to stos pozostaje w stanie niezmienionym.
Proszę zauważyć, że jest to bezpieczne nawet w przypadku
samopodstawienia s=s
.
Funkcja push
stosuje podobną technikę:
void push(const T &elem) { if(_impl._top==_impl._size) { Stack tmp(_impl._size+N); while(tmp._impl._top < _impl._top) { _impl.construct(tmp._impl._buffer+tmp._impl._top, _impl._buffer[tmp._impl._top]); ++tmp._impl._top; } _impl.swap(tmp._impl); } _impl.construct(_impl._buffer+_impl._top,elem); ++_impl._top; }
Funkcje top()
i pop()
pozostają praktycznie niezmienione, z
tym, że funkcja pop()
niszczy obiekt na wierzchołku stosu:
T &top() { if(_impl._top==0) throw std::domain_error("empty stack"); return _impl._buffer[_impl._top-1]; } void pop() { if(_impl._top==0) throw std::domain_error("empty stack"); --_impl._top; _impl.destroy(_impl._buffer+_impl._top); } bool is_empty() { return _impl._top==0; } };