Zaawansowane CPP/Wykład 15: Wyjątkowo odporny kod: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Arek (dyskusja | edycje)
Arek (dyskusja | edycje)
Nie podano opisu zmian
Linia 9: Linia 9:
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 głeboko zagnieżdżonych
<code><nowiki></nowiki></code>, niejako "tnąc" w poprzek dowolnie głeboko zagnieżdżonych
funkcji. To oczywiście jest jedną z jego podstawowych zalet, ale musimy
funkcji. 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 30: Linia 30:
Typowy przykład to niezwolnione zasoby:
Typowy przykład to niezwolnione zasoby:


  <nowiki>void f() {
  <nowiki> f() {
przydziel_zasob();
przydziel_zasob();
g(); /*może rzucić wyjątek*/
g(); /*może rzucić wyjątek*/
Linia 36: Linia 36:
}
}
</nowiki>  
</nowiki>  
Rzucenie wyjątku z <code><nowiki>g()</nowiki></code> spowoduje wyciek zasobu (zwykle pamięci).
Rzucenie wyjątku z <code><nowiki>()</nowiki></code> spowoduje wyciek zasobu (zwykle pamięci).
Taki przykład był już rozważany w wykładzie&nbsp;[[##lbl:smart_ptr|Uzupelnic lbl:smart_ptr|]].
Taki przykład był już rozważany w wykładzie&nbsp;[[##lbl:smart_ptr|Uzupelnic lbl:smart_ptr|]].
Podane tam rozwiązanie, to technika "przydział zasobu jest
Podane tam rozwiązanie, to technika "przydział zasobu jest
Linia 42: Linia 42:
klasy, której konstruktor, przydziela zasób, a destruktor zwalnia:
klasy, której konstruktor, przydziela zasób, a destruktor zwalnia:


  <nowiki>void f() {
  <nowiki> f() {
Zasob x;
Zasob x;
g(); /*może rzucić wyjątek*/
g(); /*może rzucić wyjątek*/
Linia 51: Linia 51:
może dalej pozostać nie zwolniony. Rozwiązaniem może być kod:
może dalej pozostać nie zwolniony. Rozwiązaniem może być kod:


  <nowiki>void f() {
  <nowiki> f() {
przydziel_zasob();
przydziel_zasob();
try {
try {
Linia 62: Linia 62:
</nowiki>  
</nowiki>  
po zwolnienie zasobu rzucamy (podrzucamy?) ponownie ten sam wyjątek. W
po zwolnienie zasobu rzucamy (podrzucamy?) ponownie ten sam wyjątek. W
ten sposób funkcja <code><nowiki>f()</nowiki></code> staję się "przeźroczysta dla wyjątków"
ten sposób funkcja <code><nowiki>()</nowiki></code> staję się "przeźroczysta dla wyjątków"
(exception--neutral).
(exception--neutral).


Linia 70: Linia 70:
wyjątki rzucane z konstruktora. Rozważmy następujący kod:
wyjątki rzucane z konstruktora. Rozważmy następujący kod:


  <nowiki>struct BigRsource {
  <nowiki> BigRsource {
char c[10000000];
char c[10000000];
};
};
Linia 92: Linia 92:
się stanie gdy napiszemy:
się stanie gdy napiszemy:


  <nowiki>try {
  <nowiki> {
X x;
X x;
} catch(...) {};
} catch(...) {};
</nowiki>  
</nowiki>  
Konstruktor najpierw przydzieli pamięć dla wskaźnika <code><nowiki>p1</nowiki></code>. Załóżmy,
Konstruktor najpierw przydzieli pamięć dla wskaźnika <code><nowiki></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></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></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></nowiki></code>.  Nastąpi zwinięcie stosu, ale destruktor obiektu <code><nowiki></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>_ptr</nowiki></code>:


  <nowiki>struct X {
  <nowiki> X {
   std::auto_ptr<BigResource> p1;
   std::auto_ptr<BigResource> p1;
   BadBoy      p2;
   BadBoy      p2;
Linia 118: Linia 118:
lub sami łapiemy wyjątek:
lub sami łapiemy wyjątek:


  <nowiki>struct X {
  <nowiki> X {
   BigResource *p1;
   BigResource *p1;
   BadBoy      p2;
   BadBoy      p2;
Linia 129: Linia 129:
}
}
</nowiki>  
</nowiki>  
Proszę zwrócić uwagę na blok  <code><nowiki>try</nowiki></code>, który otacza cały  
Proszę zwrócić uwagę na blok  <code><nowiki></nowiki></code>, który otacza cały  
konstruktor łącznie z listą inicjalizatorów.
konstruktor łącznie z listą inicjalizatorów.
   
   
Linia 140: Linia 140:
'''The bad. '''  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 nie powieść się destrukcja obiektu.
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
'''The ugly. '''  Obiekt jest w stanie konsystentnym, ale
Linia 151: Linia 152:
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==
Linia 159: Linia 160:
komplikacji, nie będziemy tu korzystać z klas wytycznych:
komplikacji, nie będziemy tu korzystać z klas wytycznych:


  <nowiki>template <class T,size_t N <nowiki>=</nowiki> 10> class Stack {
  <nowiki> <class T,size_t N = 10> class Stack {
   size_t nelems;
   size_t nelems;
   size_t top;
   size_t top;
Linia 169: Linia 170:
   T pop();
   T pop();


   Stack(size_t n <nowiki>=</nowiki> N);
   Stack(size_t n = N);
   &nbsp;Stack();
   &nbsp;Stack();
   Stack(const Stack&);
   Stack(const Stack&);
   Stack& operator<nowiki>=</nowiki>(const Stack&);
   Stack& operator=(const Stack&);
};
};
</nowiki>  
</nowiki>  
Linia 178: Linia 179:
Przyjrzyjmy się teraz po kolei jego metodom, zaczynając od konstruktorów:
Przyjrzyjmy się teraz po kolei jego metodom, zaczynając od konstruktorów:


  <nowiki>template <class T,size_t  N> Stack<T,N>::Stack(size_t n):
  <nowiki> <class T,size_t  N> Stack<T,N>::Stack(size_t n):
   nelems(n),top(0),v(new T[nelems]) {};
   nelems(n),top(0),v(new T[nelems]) {};
</nowiki>  
</nowiki>  
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></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></nowiki></code> samo po sobie posprząta.  Nie
mamy się co martwić stanem pozostawionego obiektu, bo jeśli konstrukcja
mamy się co 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.
Linia 189: Linia 190:
Z konstruktorem kopiującym jest już trochę gorzej:
Z konstruktorem kopiującym jest już trochę gorzej:


  <nowiki>template <class T,size_t N>  Stack<T,N>::Stack(const Stack<T,N>& s):
  <nowiki> <class T,size_t N>  Stack<T,N>::Stack(const Stack<T,N>& s):
v(new T[nelems <nowiki>=</nowiki> s.nelems]) {
v(new T[nelems = s.nelems]) {
   if( s.top > 0 )
   if( s.top > 0 )
   for(top <nowiki>=</nowiki> 0; top < s.top; top++)
   for(top = 0; top < s.top; top++)
   v[top] <nowiki>=</nowiki> s.v[top]; /* tu może zostać rzucony wyjatek */   
   v[top] = s.v[top]; /* tu może zostać rzucony wyjatek */   
}
}
</nowiki>  
</nowiki>  
Podobnie jak poprzednio, w wypadku niepowodzenia wyrażenie <code><nowiki>new</nowiki></code>
Podobnie jak poprzednio, w wypadku niepowodzenia wyrażenie <code><nowiki></nowiki></code>
posprząta po sobie. Ale wyjątek może zostać rzucony również poprzez
posprząta po sobie. Ale wyjątek może zostać rzucony również poprzez
operator przypisania klasy <code><nowiki>T</nowiki></code>. Wtedy będziemy mieli do czynienia z
operator przypisania klasy <code><nowiki></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></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>_ptr</nowiki></code> lub przechwycenie wyjątku:


  <nowiki>template <class T,size_t N>  Stack<T,N>::Stack(const Stack<T,N>& s):
  <nowiki> <class T,size_t N>  Stack<T,N>::Stack(const Stack<T,N>& s):
v(new T[nelems <nowiki>=</nowiki> s.nelems]) {
v(new T[nelems <nowiki>=</nowiki> s.nelems]) {
   try {
   try {
Linia 215: Linia 216:
}
}
</nowiki>   
</nowiki>   
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></nowiki></code> powiedzie się, tzn., że operator  
przypisania:  
przypisania:  


  <nowiki>v[top] <nowiki>=</nowiki> s.v[top];
  <nowiki>[top] <nowiki>=</nowiki> s.v[top];
</nowiki>  
</nowiki>  
pozostawił lewą stronę w stanie umożliwiającym jej destrukcję.
pozostawił lewą stronę w stanie umożliwiającym jej destrukcję.
Linia 224: Linia 225:
Sytuacja jest groźniejsza w przypadku operatora przypisania:
Sytuacja jest groźniejsza w przypadku operatora przypisania:


  <nowiki>template <class T,size_t N> Stack<T,N>&
  <nowiki> <class T,size_t N> Stack<T,N>&
Stack<T,N>::operator<nowiki>=</nowiki>(const Stack<T,N>& s) {
Stack<T,N>::operator=(const Stack<T,N>& s) {
   delete [ ] v;
   delete [ ] v;
   v <nowiki>=</nowiki> new T[nelems<nowiki>=</nowiki>s.nelems];
   v = new T[nelems=s.nelems];
   if( s.top > 0 )
   if( s.top > 0 )
   for(top <nowiki>=</nowiki> 0; top < s.top; top++)
   for(top = 0; top < s.top; top++)
     v[top] <nowiki>=</nowiki> s.v[top];
     v[top] = s.v[top];
    
    
   return *this;
   return *this;
}
}
</nowiki>  
</nowiki>  
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></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></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></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, w takim stanie w jakim go zastała
wystąpienia wyjątku zostawia stos, w takim stanie w jakim go zastała
jest podana poniżej:  
jest podana poniżej:  


  <nowiki>template <class T,size_t N> Stack<T,N>&
  <nowiki> <class T,size_t N> Stack<T,N>&
Stack<T,N>::operator<nowiki>=</nowiki>(const Stack<T,N>& s) {
Stack<T,N>::operator=(const Stack<T,N>& s) {
   T *tmp;
   T *tmp;
   try {
   try {
     tmp <nowiki>=</nowiki> new T[nelems<nowiki>=</nowiki>s.nelems];
     tmp = new T[nelems=s.nelems];
     if( s.top > 0 )
     if( s.top > 0 )
       for(size_t i <nowiki>=</nowiki> 0; i < s.top; i++)
       for(size_t i = 0; i < s.top; i++)
           tmp[i] <nowiki>=</nowiki> s.v[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<nowiki>=</nowiki>s.top;
   top=s.top;
    
    
   return *this;
   return *this;
Linia 260: Linia 261:
</nowiki>  
</nowiki>  


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></nowiki></code>:


  <nowiki>template <class T,size_t N>
  <nowiki> <class T,size_t N>
void Stack<T,N>::push(const T &element) {
void Stack<T,N>::push(const T &element) {
   if( top <nowiki>=</nowiki><nowiki>=</nowiki> nelems ) {
   if( top == nelems ) {
     T* new_buffer <nowiki>=</nowiki> new T[nelems +<nowiki>=</nowiki> N];
     T* new_buffer = new T[nelems += N];
     for(int i <nowiki>=</nowiki> 0; i < top; i++)
     for(int i = 0; i < top; i++)
       new_buffer[i] <nowiki>=</nowiki> v[i];
       new_buffer[i] = v[i];
     delete [] v;
     delete [] v;
     v <nowiki>=</nowiki> new_buffer;
     v = new_buffer;
   }
   }
      
      
   v[top++] <nowiki>=</nowiki> element;
   v[top++] = element;
}
}
</nowiki>  
</nowiki>  
Linia 278: Linia 279:
wykonywane jest więc tylko polecenie:
wykonywane jest więc tylko polecenie:


  <nowiki>v[top++] <nowiki>=</nowiki> element;
  <nowiki>[top++] <nowiki>=</nowiki> element;
</nowiki>  
</nowiki>  
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></nowiki></code> zostanie
zwiększone. Lepiej jest więc napisać:
zwiększone. Lepiej jest więc napisać:


  <nowiki>v[top] <nowiki>=</nowiki> element;
  <nowiki>[top] <nowiki>=</nowiki> element;
   ++top;
   ++top;
</nowiki>  
</nowiki>  
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></nowiki></code> zostawi nas ze zwiększonym polem
<code><nowiki>nelems</nowiki></code> pomimo, że pamięć się nie zwiększyła. Wyjątek z operatora
<code><nowiki></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ęć
przydzialona do <code><nowiki>new_buffer</nowiki></code> nigdy nie zostanie zwolniona.
przydzialona do <code><nowiki>_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></nowiki></code> następująco:


  <nowiki>template <class T,size_t N>
  <nowiki> <class T,size_t N>
void Stack<T,N>::push(T element) {
void Stack<T,N>::push(T element) {
   if( top <nowiki>=</nowiki><nowiki>=</nowiki> nelems ) {
   if( top == nelems ) {
     T* new_buffer;
     T* new_buffer;
     size_t new_nelems;
     size_t new_nelems;
     try {
     try {
       new_nelems<nowiki>=</nowiki>nelems+N;
       new_nelems=nelems+N;
       new_buffer <nowiki>=</nowiki> new T[new_nelems];
       new_buffer = new T[new_nelems];
       for(int i <nowiki>=</nowiki> 0; i < top; i++)
       for(int i = 0; i < top; i++)
         new_buffer[i] <nowiki>=</nowiki> v[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 <nowiki>=</nowiki> new_nelems;
       nelems = new_nelems;
     }
     }


   v[top] <nowiki>=</nowiki> element;
   v[top] = element;
   ++top;
   ++top;
   }
   }
</nowiki>   
</nowiki>   


Na koniec została nam jeszcze funkcja <code><nowiki>pop</nowiki></code>:
Na koniec została nam jeszcze funkcja <code><nowiki></nowiki></code>:


  <nowiki>template <class T,size_t N> T Stack<T,N>::pop() {
  <nowiki> <class T,size_t N> T Stack<T,N>::pop() {
   if( top <nowiki>=</nowiki><nowiki>=</nowiki> 0 )
   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 */
}
}
</nowiki>  
</nowiki>  
Jak widać funkcja <code><nowiki>pop</nowiki></code> może rzucić jawnie wyjątek
Jak widać funkcja <code><nowiki></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>::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:


  <nowiki>return v[--top]; /* tu może nastąpić kopiowanie */
  <nowiki> v[--top]; /* tu może nastąpić kopiowanie */
</nowiki>  
</nowiki>  
Ponieważ zwracamy <code><nowiki>v[--top]</nowiki></code> przez wartość, to może nastąpić
Ponieważ zwracamy <code><nowiki>[--top]</nowiki></code> przez wartość, to może nastąpić
kopiowanie elementy typu <code><nowiki>T</nowiki></code>. Nie musi, ponieważ kompilator ma prawo
kopiowanie elementy typu <code><nowiki></nowiki></code>. Nie musi, ponieważ kompilator ma prawo
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></nowiki></code> zostanie zmniejszona.  
Rozważmy też wyrażenie:
Rozważmy też wyrażenie:


  <nowiki>x <nowiki>=</nowiki> s.pop();  
  <nowiki> <nowiki>=</nowiki> s.pop();  
</nowiki>  
</nowiki>  
Jeżeli operacja przypisania się nie powiedzie, to stracimy
Jeżeli operacja przypisania się nie powiedzie, to stracimy
Linia 344: Linia 345:
modyfikujące stan stosu od operacji tylko ten stan odczytujących:
modyfikujące stan stosu od operacji tylko ten stan odczytujących:


  <nowiki>template <class T,size_t N> void Stack<T,N>::pop() {
  <nowiki> <class T,size_t N> void Stack<T,N>::pop() {
   if( top <nowiki>=</nowiki><nowiki>=</nowiki> 0 )
   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 <nowiki>=</nowiki><nowiki>=</nowiki> 0 )
   if( top == 0 )
     throw std::domain_error("pop on empty stack");
     throw std::domain_error("pop on empty stack");


Linia 356: Linia 357:
}
}
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 <nowiki>=</nowiki><nowiki>=</nowiki> 0 )
   if( top == 0 )
     throw std::domain_error("pop on empty stack");
     throw std::domain_error("pop on empty stack");


Linia 362: Linia 363:
}
}
</nowiki>  
</nowiki>  
W przeciwieństwie do <code><nowiki>pop()</nowiki></code> operacja <code><nowiki>top()</nowiki></code> może zwracać
W przeciwieństwie do <code><nowiki>()</nowiki></code> operacja <code><nowiki>()</nowiki></code> może zwracać
wartość przez referencje. Funkcja <code><nowiki>pop()</nowiki></code>  robić tego w ogólności
wartość przez referencje. Funkcja <code><nowiki>()</nowiki></code>  robić tego w ogólności
nie mogła, bo potencjalnie niszczyła obiekt zdejmowany ze stosu.  
nie mogła, bo potencjalnie niszczyła obiekt zdejmowany ze stosu.  


Linia 369: Linia 370:


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></nowiki></code> posiadał:
* Konstruktor domyślny  
* Konstruktor domyślny  
* Bezpieczny (względem wyjątków) operator przypisania
* Bezpieczny (względem wyjątków) operator przypisania
Linia 379: Linia 380:
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></nowiki></code>, która od typu <code><nowiki></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 385: Linia 386:
Zaczniemy od zdefiniowania  pomocniczej klasy do zarządzania pamięcią:
Zaczniemy od zdefiniowania  pomocniczej klasy do zarządzania pamięcią:


  <nowiki>template<typename T,typename Allocator <nowiki>=</nowiki> std::allocator<T> >  
  <nowiki><typename T,typename Allocator <nowiki>=</nowiki> std::allocator<T> >  
struct  Stack_impl : public Allocator{  
struct  Stack_impl : public Allocator{  
   size_t _top;
   size_t _top;
Linia 411: Linia 412:
</nowiki>  
</nowiki>  
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>()</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></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></nowiki></code> korzysta z klasy <code><nowiki>_impl</nowiki></code>:   


  <nowiki>template<typename T,size_t N <nowiki>=</nowiki> 10,
  <nowiki><typename T,size_t N = 10,
         typename Allocator <nowiki>=</nowiki> std::allocator<T> >  
         typename Allocator = std::allocator<T> >  
class Stack {
class Stack {
private:
private:
Linia 428: Linia 429:
Konstruktory:  
Konstruktory:  


  <nowiki>public:
  <nowiki>:
   Stack(size_t n <nowiki>=</nowiki> N):_impl(n) {};
   Stack(size_t n <nowiki>=</nowiki> N):_impl(n) {};


Linia 442: Linia 443:
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, zostanie rzucony wyjątek w funkcji
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></nowiki></code>, to wywołany podczas zwijania stosu destruktor <code><nowiki>_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":


  <nowiki>Stack &operator<nowiki>=</nowiki>(const Stack& rhs) {
  <nowiki> &operator=(const Stack& rhs) {
     Stack tmp(rhs);
     Stack tmp(rhs);
     _impl.swap(tmp._impl);
     _impl.swap(tmp._impl);
Linia 455: Linia 456:
</nowiki>  
</nowiki>  
Tworzymy kopie prawej strony i zamieniamy z lewą stroną. Obiekt
Tworzymy kopie prawej strony i zamieniamy z lewą stroną. Obiekt
<code><nowiki>tmp</nowiki></code> jest obiektem lokalnym, więc zostanie zniszczony.  Jeśli nie
<code><nowiki></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<nowiki>=</nowiki>s</nowiki></code>.  
samopodstawienia <code><nowiki>=s</nowiki></code>.  


Funkcja <code><nowiki>push</nowiki></code> stosuje podobną technikę:
Funkcja <code><nowiki></nowiki></code> stosuje podobną technikę:


  <nowiki>void push(const T &elem) {
  <nowiki> push(const T &elem) {
     if(_impl._top<nowiki>=</nowiki><nowiki>=</nowiki>_impl._size) {
     if(_impl._top==_impl._size) {


       Stack tmp(_impl._size+N);
       Stack tmp(_impl._size+N);
Linia 480: Linia 481:
</nowiki>   
</nowiki>   


Funkcje <code><nowiki>top()</nowiki></code> i <code><nowiki>pop()</nowiki></code> pozostają praktycznie niezmienione, z
Funkcje <code><nowiki>()</nowiki></code> i <code><nowiki>()</nowiki></code> pozostają praktycznie niezmienione, z
tym, że funkcja <code><nowiki>pop()</nowiki></code> niszczy obiekt na wierzchołku stosu:
tym, że funkcja <code><nowiki>()</nowiki></code> niszczy obiekt na wierzchołku stosu:


  <nowiki>T &top() {
  <nowiki> &top() {
     if(_impl._top<nowiki>=</nowiki><nowiki>=</nowiki>0)
     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];
Linia 490: Linia 491:


   void pop() {
   void pop() {
     if(_impl._top<nowiki>=</nowiki><nowiki>=</nowiki>0)
     if(_impl._top==0)
       throw std::domain_error("empty stack");
       throw std::domain_error("empty stack");
      
      
Linia 498: Linia 499:


   bool is_empty() {
   bool is_empty() {
     return _impl._top<nowiki>=</nowiki><nowiki>=</nowiki>0;
     return _impl._top==0;
   }
   }



Wersja z 12:41, 1 wrz 2006

Uwaga: przekonwertowane latex2mediawiki; prawdopodobnie trzeba wprowadzi� poprawki

{Wyjątkowo odporny kod}

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 , niejako "tnąc" w poprzek dowolnie głeboko zagnieżdżonych funkcji. 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 hierarchi 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:

 f() {
przydziel_zasob();
g(); /*może rzucić wyjątek*/
zwolnij_zasob(); 
}
 

Rzucenie wyjątku z () spowoduje wyciek zasobu (zwykle pamięci). Taki przykład był już rozważany w wykładzie Uzupelnic lbl:smart_ptr|. 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:

 f() {
Zasob x;
g(); /*może rzucić wyjątek*/
} /* niejawnie wyływany  destruktor x. Zasob() */
 

Wtedy podczas zwijanie stosu zasób zostanie zwolniony automatycznie. Proszę zauważyć jednak, że jeśli nie przechwycimy wyjątku, to zasób może dalej pozostać nie zwolniony. Rozwiązaniem może być kod:

 f() {
przydziel_zasob();
try {
g(); /*może rzucić wyjątek*/
}
cath(...) {zwolnij_zasob();throw;}
 
zwolnij_zasob(); 
}
 

po zwolnienie zasobu rzucamy (podrzucamy?) ponownie ten sam wyjątek. W ten sposób funkcja () staję 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:

 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ędowy 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:

 {
X x;
} catch(...) {};
 

Konstruktor najpierw przydzieli pamięć dla wskaźnika . Załóżmy, że ta alokacja się powiedzie. Następnie zostanie wywołany konstruktor , który rzuci wyjątek. Wyjątek nie zostanie złapany w konstruktorze , więc sterowanie zostane przekazane do klauzuli . Nastąpi zwinięcie stosu, ale destruktor obiektu 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 _ptr:

 X {
  std::auto_ptr<BigResource> p1;
  BadBoy      p2;
  X: p1(new  BigResource) {}

 X() {
  delete p1;
  }
};
 

lub sami łapiemy wyjątek:

 X {
  BigResource *p1;
  BadBoy      p2;
  X: try {p1(new BigResource) {}}
  catch(...){delete p1;};

 X() {
  delete p1;
  }
}
 

Proszę zwrócić uwagę na blok , 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 Uzupelnic lbl:wytyczne|. Żeby nie wprowadzać komplikacji, nie będziemy tu korzystać z klas wytycznych:

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

Przyjrzyjmy się teraz po kolei jego metodom, zaczynając od konstruktorów:

 <class T,size_t  N> Stack<T,N>::Stack(size_t n):
  nelems(n),top(0),v(new T[nelems]) {};
 

W powyższym konstruktorze może nie powieść się tylko operacja tworzenia tablicy . Ale wtedy zgodnie z tym co już omawialiśmy w poprzednim wykładzie, wyrażenie samo po sobie posprząta. Nie mamy się co 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:

 <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 posprząta po sobie. Ale wyjątek może zostać rzucony również poprzez operator przypisania klasy . Wtedy będziemy mieli do czynienia z wyciekiem pamięci, ponieważ nie zostanie wywołany destruktor stosu, który zwalnia pamięć . Taki przykład już omawialiśmy na początku wykładu. Rozwiązaniem jest użycie _ptr lub przechwycenie wyjątku:

 <class T,size_t N>  Stack<T,N>::Stack(const Stack<T,N>& s):
v(new T[nelems <nowiki>= 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 ;
 }

} </nowiki> To rozwiązanie zakłada, że destrukcja powiedzie się, tzn., że operator przypisania:

[top] <nowiki>= s.v[top];

</nowiki> pozostawił lewą stronę w stanie umożliwiającym jej destrukcję.

Sytuacja jest groźniejsza w przypadku operatora przypisania:

 <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 zostawia stos w stanie złym z wiszącym luźno wskaźnikiem . Wyjątek rzucony przez operator przypisania elementów tablicy 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:

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

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

[top++] <nowiki>= element;

</nowiki> Jak już zauważyliśmy, przypisanie może się nie powieść, wtedy stos zostanie w stanie złym lub niezdefiniowanym, ponieważ zostanie zwiększone. Lepiej jest więc napisać:

[top] <nowiki>= element;
 ++top;

</nowiki> Zobaczmy, co się dzieje, jeśli zażądamy zwiększenia pamięci. Niepowodzenie wyrażenia zostawi nas ze zwiększonym polem pomimo, że pamięć się nie zwiększyła. Wyjątek z operatora przypisania zostawi nas z wyciekiem pamięci, ponieważ pamięć przydzialona do _buffer nigdy nie zostanie zwolniona. Uwzględaniając te uwagi, poprawimy funkcję następująco:

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

 <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 może rzucić jawnie wyjątek ::domain_error. Z tym wyjątkiem nie ma problemów. Potencjalny problem stwarza za to wyrażenie:

 v[--top]; /* tu może nastąpić kopiowanie */
 

Ponieważ zwracamy [--top] przez wartość, to może nastąpić kopiowanie elementy typu . 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ść zostanie zmniejszona. Rozważmy też wyrażenie:

 <nowiki>= s.pop(); 

</nowiki> 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 prosty rozdzielić operację modyfikujące stan stosu od operacji tylko ten stan odczytujących:

 <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 () operacja () może zwracać wartość przez referencje. Funkcja () 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 posiadał:

  • Konstruktor domyślny
  • Bezpieczny (względem wyjątków) operator przypisania
  • Desktruktor nierzucają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 , która od typu 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ą:

<typename T,typename Allocator <nowiki>= 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);
 }

}; </nowiki> Jedyne miejsce, gdzie może zostać rzucony wyjątek to funkcja (), ale wtedy żadna pamięć nie zostanie przydzielona ani żaden obiekt nie zostanie stworzony. Korzystamy tu też z żądania, aby alokator był bezstanowy, inaczej funkcja musiałaby też zamieniać składowe alokatorów.

Klasa korzysta z klasy _impl:

<typename T,size_t N = 10,
         typename Allocator = std::allocator<T> > 
class Stack {
private:
  Stack_impl<T,Allocator> _impl;
/* {mod15/code/stack_sutter.h}{stacksutter.h} */
 

Konstruktory:

:
  Stack(size_t n <nowiki>= 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;
   }
 } 

/* {mod15/code/stack_sutter.h}{stacksutter.h} */ </nowiki> 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 , to wywołany podczas zwijania stosu destruktor _impl wywoła destruktory stworzonych obiektów i zwolni pamięć.

Operator przypisania korzysta z "triku":

 &operator=(const Stack& rhs) {
    Stack tmp(rhs);
    _impl.swap(tmp._impl);
    return *this;
  }
/* {mod15/code/stack_sutter.h}{stacksutter.h} */
 

Tworzymy kopie prawej strony i zamieniamy z lewą stroną. Obiekt 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.

Funkcja stosuje podobną technikę:

 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;
  }
/* {mod15/code/stack_sutter.h}{stacksutter.h} */
   

Funkcje () i () pozostają praktycznie niezmienione, z tym, że funkcja () niszczy obiekt na wierzchołku stosu:

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

};
/* {mod15/code/stack_sutter.h}{stacksutter.h} */