Zaawansowane CPP/Wykład 13: Wyjątki: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Arek (dyskusja | edycje)
Arek (dyskusja | edycje)
Nie podano opisu zmian
Linia 1: Linia 1:
''Uwaga: przekonwertowane latex2mediawiki; prawdopodobnie trzeba wprowadzi� poprawki''
''Uwaga: przekonwertowane latex2mediawiki; prawdopodobnie trzeba wprowadzić poprawki''


==Wstęp==
==Wstęp==
Linia 27: Linia 27:
akapitów na zastanowienie się, czy w ogóle należy będy wykrywać i
akapitów na zastanowienie się, czy w ogóle należy będy wykrywać i
obsługiwać.  Nawet jeśli większość z państwa krzyknie "oczywiście, że
obsługiwać.  Nawet jeśli większość z państwa krzyknie "oczywiście, że
tak" (choć podejrzewam że większość tego sama nie robi:{kto
tak" (choć podejrzewam że większość tego sama nie robi: kto ostatnio sprawdzał wartość zwróconą przez funkcję <code>printf</code>?), to
  ostatnio sprawdzał wartość zwróconą przez funkcję <code>printf</code>?}, to
i tak pozostaje pytanie jakie błedy będziemy starali się wykrywać.
i tak pozostaje pytanie jakie błedy będziemy starali się wykrywać.


Linia 40: Linia 39:
Nawet jednak w tej ostatniej sytuacji, różne formy obsługi błędów  
Nawet jednak w tej ostatniej sytuacji, różne formy obsługi błędów  
mogą  nam bardzo pomóc w debugowaniu. Linijki typu:
mogą  nam bardzo pomóc w debugowaniu. Linijki typu:
   
 
  if( NULL<nowiki>=</nowiki><nowiki>=</nowiki>(fin<nowiki>=</nowiki>fopen(input_file_name,"r"))) {  
  if( NULL<nowiki>=</nowiki><nowiki>=</nowiki>(fin<nowiki>=</nowiki>fopen(input_file_name,"r"))) {  
     fprintf(stderr,"cannot open file input_file_name);
     fprintf(stderr,"cannot open file input_file_name);
     exit(1); }
     exit(1); }
  }
  }
 
lub
lub
   
 
  if( NULL<nowiki>=</nowiki><nowiki>=</nowiki>(p<nowiki>=</nowiki>malloc(n_bytes))) {  
  if( NULL<nowiki>=</nowiki><nowiki>=</nowiki>(p<nowiki>=</nowiki>malloc(n_bytes))) {  
     fprintf(stderr,"cannot allocate memory for ...");
     fprintf(stderr,"cannot allocate memory for ...");
     exit(1);  
     exit(1);  
   }
   }
  }
  }
 
mogą nam oszczedzić, jeśli nie godzin, to wielu minut frustracji.
mogą nam oszczedzić, jeśli nie godzin, to wielu minut frustracji.
Proszę zwrócić uwagę że oba przykłady dotyczą  zasobów
Proszę zwrócić uwagę że oba przykłady dotyczą  zasobów
Linia 74: Linia 73:
często wykonywane. Z drugiej strony  błedy przekroczenia zakresu
często wykonywane. Z drugiej strony  błedy przekroczenia zakresu
są bardzo "wredne". Rozważmy np. taki prosty kod:
są bardzo "wredne". Rozważmy np. taki prosty kod:
   
 
  Stack<int,5> s;
  Stack<int,5> s;
    
    
   for(int i<nowiki>=</nowiki>0;i<1000;i++)
   for(int i<nowiki>=</nowiki>0;i<1000;i++)
Linia 84: Linia 83:
     std::cerr<<++i<<" "<<s.pop()<<"";
     std::cerr<<++i<<" "<<s.pop()<<"";
  /* {mod13/code/overflow.cpp}{overflow.cpp} */
  /* {mod13/code/overflow.cpp}{overflow.cpp} */
 
Na moim komputerze powyższy program, wykonał 20981 operacji <code>pop()</code>,
Na moim komputerze powyższy program, wykonał 20981 operacji <code>pop()</code>,
zanim padł z komunikatem <tt>Naruszenie ochrony pamięci</tt>.  Proszę
zanim padł z komunikatem <tt>Naruszenie ochrony pamięci</tt>.  Proszę
Linia 151: Linia 150:
instrukcje rzucające wyjątki, w przypadku przekroczenia zakresu
instrukcje rzucające wyjątki, w przypadku przekroczenia zakresu
(dla prostoty nie będę korzystał z klas wytycznych):
(dla prostoty nie będę korzystał z klas wytycznych):
 
  template<typename T <nowiki>=</nowiki> int , size_t N <nowiki>=</nowiki> 100> class Stack {
  template<typename T <nowiki>=</nowiki> int , size_t N <nowiki>=</nowiki> 100> class Stack {
  private:
  private:
Linia 173: Linia 172:
  };
  };
  /* {mode13/code/stack_except.h}{stackexcept.h}*/
  /* {mode13/code/stack_except.h}{stackexcept.h}*/
 
Polecenie <code>throw</code> służy właśnie do rzucania wyjątków. W tym wypadku  
Polecenie <code>throw</code> służy właśnie do rzucania wyjątków. W tym wypadku  
rzucane są stałe napisowe, które będą automatycznie konwertowane na typ  
rzucane są stałe napisowe, które będą automatycznie konwertowane na typ  
<code>const char *</code>.   
<code>const char *</code>.   
Wykonanie programu
Wykonanie programu
 
  main() {
  main() {
   Stack<int,5> s;
   Stack<int,5> s;
Linia 189: Linia 188:
  }
  }
  /* {mod13/code/overflow.cpp}{overflow.cpp} */   
  /* {mod13/code/overflow.cpp}{overflow.cpp} */   
 
spowoduje przerwanie programu w trakcie wykonywania drugiego polecenia
spowoduje przerwanie programu w trakcie wykonywania drugiego polecenia
<code>pop</code>. Komunikat, który się przy tym pojawia jest zależny
<code>pop</code>. Komunikat, który się przy tym pojawia jest zależny
Linia 195: Linia 194:


Wyjątki można łapać korzystając z bloku <code>try</code>.
Wyjątki można łapać korzystając z bloku <code>try</code>.
   
 
  Stack<int,5> s;
  Stack<int,5> s;
   s.push(1);
   s.push(1);
   try {
   try {
Linia 213: Linia 212:
   }
   }
  /* {mod13/code/stack_except.cpp}{stackexcept.cpp} */
  /* {mod13/code/stack_except.cpp}{stackexcept.cpp} */
 
W bloku <code>try</code> umieszczamy instrukcje które mogą potencjalnie rzucić
W bloku <code>try</code> umieszczamy instrukcje które mogą potencjalnie rzucić
wyjątek. Za blokiem <code>try</code> umieszczamy jedną lub więcej klauzul <code>catch</code>
wyjątek. Za blokiem <code>try</code> umieszczamy jedną lub więcej klauzul <code>catch</code>
Linia 223: Linia 222:
Przyjrzyjmy się teraz dokladniej mechanizmowi rzucania i łapania
Przyjrzyjmy się teraz dokladniej mechanizmowi rzucania i łapania
wyjątków.  Rozważmy prosty przykład:
wyjątków.  Rozważmy prosty przykład:
 
  struct X {
  struct X {
   int val;
   int val;
Linia 250: Linia 249:
  }
  }
  /* {mod13/code/caught.cpp}{caught.cpp} */
  /* {mod13/code/caught.cpp}{caught.cpp} */
 
Oto wynik wykonania tego programu:
Oto wynik wykonania tego programu:
 
  constructing 2
  constructing 2
  constructing 3
  constructing 3
Linia 261: Linia 260:
  main
  main
  destructing 2
  destructing 2
 
Co możemy zauważyć?  
Co możemy zauważyć?  
# Wyjątek przerwał wykonywanie funkcji <code>f()</code> i bloku <code>try</code>, sterowanie zostało przekazana do klauzuli <code>catch(int)</code>.  
# Wyjątek przerwał wykonywanie funkcji <code>f()</code> i bloku <code>try</code>, sterowanie zostało przekazana do klauzuli <code>catch(int)</code>.  
Linia 269: Linia 268:
Klauzula <code>catch(...)</code> wyłapuje każdy wyjatek, jeśli np. pominiemy
Klauzula <code>catch(...)</code> wyłapuje każdy wyjatek, jeśli np. pominiemy
klauzule <code>catch(int)</code>:
klauzule <code>catch(int)</code>:
 
  catch(double){cout<<"zlapalem double-a";}
  catch(double){cout<<"zlapalem double-a";}
  //catch(int){cout<<"zlapalem int-a";}
  //catch(int){cout<<"zlapalem int-a";}
  catch(...){cout<<"zlapalem cos ";}
  catch(...){cout<<"zlapalem cos ";}
 
to wynikiem wywołania programu będzie:
to wynikiem wywołania programu będzie:
 
  constructing 2
  constructing 2
  constructing 3
  constructing 3
Linia 284: Linia 283:
  main
  main
  destructing 2
  destructing 2
 
Z tego przykładu widać też, że w przypadku dopasowywania klauzul
Z tego przykładu widać też, że w przypadku dopasowywania klauzul
<code>catch</code> nie następuje niejawna konwersja argumentów.
<code>catch</code> nie następuje niejawna konwersja argumentów.
Linia 292: Linia 291:
A co się stanie, jeśli wyjątku nie złapiemy ? Zeby się o tym przekonać
A co się stanie, jeśli wyjątku nie złapiemy ? Zeby się o tym przekonać
usuniemy kolejną klauzulę <code>catch</code>:
usuniemy kolejną klauzulę <code>catch</code>:
 
  catch(double){cout<<"zlapalem double-a";}
  catch(double){cout<<"zlapalem double-a";}
  //catch(int){cout<<"zlapalem int-a";}
  //catch(int){cout<<"zlapalem int-a";}
  //catch(...){cout<<"zlapalem cos ";}
  //catch(...){cout<<"zlapalem cos ";}
 
Wynik programu jest teraz zupełnie inny:
Wynik programu jest teraz zupełnie inny:
 
  constructing 2
  constructing 2
  constructing 3
  constructing 3
Linia 304: Linia 303:
  terminate called after throwing an instance of 'int'
  terminate called after throwing an instance of 'int'
  Abort
  Abort
 
Niezłapany wyjątek spowodował wywołanie funkcji <code>abort()</code>, która
Niezłapany wyjątek spowodował wywołanie funkcji <code>abort()</code>, która
zakończyła program bez wywołania  destruktorów. Sciśle rzecz
zakończyła program bez wywołania  destruktorów. Sciśle rzecz
Linia 316: Linia 315:
Domyślne zachowanie funkcji <code>terminate()</code> można zmienić, ustawiając
Domyślne zachowanie funkcji <code>terminate()</code> można zmienić, ustawiając
własną funkcje, za pomocą:
własną funkcje, za pomocą:
 
  namespace std {
  namespace std {
  typedef void (*terminate_handler)(void);
  typedef void (*terminate_handler)(void);
  terminate_handler set_terminate(terminate_handler new_terminate);
  terminate_handler set_terminate(terminate_handler new_terminate);
  }
  }
 
Funkcja ustawiana w tym poleceniu nie może zwrócić sterowania, taka
Funkcja ustawiana w tym poleceniu nie może zwrócić sterowania, taka
próba kończy sie wywołaniem funkcji <code>abort()</code>. Oznacza to, że funkcja
próba kończy sie wywołaniem funkcji <code>abort()</code>. Oznacza to, że funkcja
<code>new_terminate()</code> musi kończyć się wywołaniem <code>abort()</code> lub
<code>new_terminate()</code> musi kończyć się wywołaniem <code>abort()</code> lub
<code>exit()</code>.
<code>exit()</code>.
 
  void my_terminate() {std::cerr<<"terminating "<<std::endl;exit(1);}
  void my_terminate() {std::cerr<<"terminating "<<std::endl;exit(1);}
   
   
Linia 334: Linia 333:
  }
  }
  /* {mod1/code/terminate.cpp}{terminate.cpp}*/
  /* {mod1/code/terminate.cpp}{terminate.cpp}*/
 
==Wyjątki w destruktorach==
==Wyjątki w destruktorach==


Linia 343: Linia 342:
szczególności stanie się to jeśli któryś z destruktorów wywoływanych w
szczególności stanie się to jeśli któryś z destruktorów wywoływanych w
trakcie zwijania stosu rzuci wyjątek:
trakcie zwijania stosu rzuci wyjątek:
 
  struct X {
  struct X {
   &nbsp;X() {
   &nbsp;X() {
Linia 364: Linia 363:
  }
  }
  /* {mode13/code/destruktor.cpp}{destruktor.cpp} */
  /* {mode13/code/destruktor.cpp}{destruktor.cpp} */
 
W powyższym kodzie destruktor klasy <code>X</code> jest wołany dwa razy. Po raz
W powyższym kodzie destruktor klasy <code>X</code> jest wołany dwa razy. Po raz
pierwszy, podczas wychodzenia z pierwszego bloku <code>try</code>. Jest to
pierwszy, podczas wychodzenia z pierwszego bloku <code>try</code>. Jest to
Linia 392: Linia 391:
   
   
Można z niej korzystać np. następująco:
Można z niej korzystać np. następująco:
 
  main() {
  main() {
   try {
   try {
Linia 408: Linia 407:
  }
  }
  /* {mod13/code/hierarchy.cpp}{hierarchy.cpp}
  /* {mod13/code/hierarchy.cpp}{hierarchy.cpp}
 
Kolejność klauzul <code>catch</code> jest ważna, ponieważ klauzule są
Kolejność klauzul <code>catch</code> jest ważna, ponieważ klauzule są
sprawdzane po kolei, i pierwsza która pasuje zostanie wykonana.
sprawdzane po kolei, i pierwsza która pasuje zostanie wykonana.
Linia 422: Linia 421:
funkcji.  Służy do tego deklaracja <code>throw(...)</code> umieszana za
funkcji.  Służy do tego deklaracja <code>throw(...)</code> umieszana za
deklaracją listy argumentów funkcji np.:
deklaracją listy argumentów funkcji np.:
void no_throw(int) throw();
   
   
void no_throw(int) throw();
 
deklaruje, że funkcja <code>no_throw</code> nie rzuci żadnego wyjątku, natomiast
deklaruje, że funkcja <code>no_throw</code> nie rzuci żadnego wyjątku, natomiast
void throw_std(int) throw(exception);
   
   
void throw_std(int) throw(exception);
 
deklaruje, że funkcja <code>throw_std</code> będzie rzucać tylko wyjątki ze
deklaruje, że funkcja <code>throw_std</code> będzie rzucać tylko wyjątki ze
standardowej hierarchi. Brak deklaracji oznacza, że funkcja może rzucać
standardowej hierarchi. Brak deklaracji oznacza, że funkcja może rzucać
Linia 436: Linia 435:
by mogły wymusić konsystencję tych deklaracji. Rozważmy implementację  
by mogły wymusić konsystencję tych deklaracji. Rozważmy implementację  
funkcji:
funkcji:
 
  void f(int i) {throw i;}
  void f(int i) {throw i;}
  void g() throw(int) {throw 0;};  
  void g() throw(int) {throw 0;};  
  void no_throw(int i) {f(i;};
  void no_throw(int i) {f(i;};
 
Funkcja <code>f</code> proklamuje całemu światu, że może rzucać wyjątek
Funkcja <code>f</code> proklamuje całemu światu, że może rzucać wyjątek
(poprzez brak deklaracji, że nie może), podobnie funkcja <code>g()</code>,
(poprzez brak deklaracji, że nie może), podobnie funkcja <code>g()</code>, a pomimo to kompilator nie
a pomimo to kompilator nie
pozwala na to, aby wywołać ją wewnątrz funkcji która jawnie deklaruje,
pozwala na to, aby wywołać ją wewnątrz funkcji która jawnie deklaruje,
że wyjątku nie rzuci. Proszę to porównać np. ze zastosowaniem
że wyjątku nie rzuci. Proszę to porównać np. ze zastosowaniem
Linia 475: Linia 473:
wyjątek.  W ten sposób możemy jej użyć do "podmiany"
wyjątek.  W ten sposób możemy jej użyć do "podmiany"
niespodziewanego wyjątku na inny. Zobaczmy jak to działa:
niespodziewanego wyjątku na inny. Zobaczmy jak to działa:
 
  void f() throw(int) {
  void f() throw(int) {
   throw "niespodzianka!";
   throw "niespodzianka!";
Linia 487: Linia 485:
  }
  }
  /* {mod13/code/unexpected.cpp}{unexpected.cpp} */
  /* {mod13/code/unexpected.cpp}{unexpected.cpp} */
 
Ponieważ <code>f()</code>  rzuca <code>const char *</code>, a deklaruje tylko <code>int</code>-a,
Ponieważ <code>f()</code>  rzuca <code>const char *</code>, a deklaruje tylko <code>int</code>-a,
powyższy program wywoła funkcję <code>unexpected()</code>, która przerwie program.
powyższy program wywoła funkcję <code>unexpected()</code>, która przerwie program.
Jeżeli podmienimy wyjątek poprzez ustawienie odpowiedniej funkcji:
Jeżeli podmienimy wyjątek poprzez ustawienie odpowiedniej funkcji:
 
  void unexpected_handler() {throw 0;};
  void unexpected_handler() {throw 0;};
  std::set_unexpected(unexpected_handler);
  std::set_unexpected(unexpected_handler);
 
to zamiast <code>const char *</code> zostanie rzucony <code>int</code> i złapany przez
to zamiast <code>const char *</code> zostanie rzucony <code>int</code> i złapany przez
klauzulę <code>catch(...)</code>. Tak się stanie ponieważ <code>int</code> jest na
klauzulę <code>catch(...)</code>. Tak się stanie ponieważ <code>int</code> jest na
Linia 503: Linia 501:
zadeklarowanych wyjątków funkcji <code>f()</code> jest podmienieny na
zadeklarowanych wyjątków funkcji <code>f()</code> jest podmienieny na
<code>std::bad_exception()</code>:
<code>std::bad_exception()</code>:
 
  void f() throw(int,std::bad_exception) {
  void f() throw(int,std::bad_exception) {
   throw "niespodzianka!";
   throw "niespodzianka!";
Linia 526: Linia 524:
sprawdzić jak się sprawy mają. W tym celu skorzystałem z następujących  
sprawdzić jak się sprawy mają. W tym celu skorzystałem z następujących  
programików:
programików:
 
  double scin(double x,bool flag) {
  double scin(double x,bool flag) {
   if(flag) throw 0;
   if(flag) throw 0;
Linia 541: Linia 539:
  }
  }
  /* {mod13/code/exceptions.cpp}{exceptions.cpp} */
  /* {mod13/code/exceptions.cpp}{exceptions.cpp} */
 
oraz
oraz
 
  double scin(double x,bool flag) {
  double scin(double x,bool flag) {
   if(flag) return 0;
   if(flag) return 0;
Linia 556: Linia 554:
  }
  }
  /* {mod13/code/no_exceptions.cpp}{noexceptions.cpp} */
  /* {mod13/code/no_exceptions.cpp}{noexceptions.cpp} */
 
Jak widać drugi z nich nie ma nawet śladu wyjątków. W pierwszym podejściu  
Jak widać drugi z nich nie ma nawet śladu wyjątków. W pierwszym podejściu  
ustawilem flagę <code>f</code> na zero, co powodowało że żaden wyjątek nie był rzucany.
ustawilem flagę <code>f</code> na zero, co powodowało że żaden wyjątek nie był rzucany.
Linia 587: Linia 585:


==Kiedy rzucać wyjątki==
==Kiedy rzucać wyjątki==
==Podsumowanie==
==Wstęp==
Jesteśmy istotami omylnymi, więc niezależnie od naszych starań nasze
programy bedą zawierały usterki, dostarczane do nich dane nie zawsze
będą poprawne, a i sprzęt nie zawsze będzie działał tak jak trzeba.
Nie oznacza to, że należy zaniechać dążenia do pisania bezbłednego kodu,
wprost przeciwnie, jakość kodu powinna być jednym z naszych
piorytetów, ale należy się też pogodzić z faktem, że błędy bedą
występować i powinniśmy być na takie sytuacje przygotowani, jak to
mówią "najwyższą formą zaufania jest kontrola".
Na potrzeby tego wykładu zdefiniujemy bardzo luźno błąd jako
wystąpienie sytuacji, która wystąpić nie powinna. Nie będziemy też
interesować się bardzo tym jak błedy wykrywać, ale raczej co zrobić
kiedy takowy wykryjemy. W następnym podrozdziale omówię bardzo
pobieżnie różne możliwości reakcji na wystąpienie błedu i wprowadzę
pojęcie wyjątku. Reszta wykładu będzie poświęcona zagadnieniom związanym
z pisaniem kodu używającego wyjątki.
==Wykrywanie błedów==
Jak już napisałem we wstępie chciąłbym od razu przejść do sytuacji, w
której wiemy że wystapił błąd. Jednak muszę poświęcić najpierw kilka
akapitów na zastanowienie się, czy w ogóle należy będy wykrywać i
obsługiwać.  Nawet jeśli większość z państwa krzyknie "oczywiście, że
tak" (choć podejrzewam że większość tego sama nie robi:{kto
  ostatnio sprawdzał wartość zwróconą przez funkcję <code>printf</code>?}, to
i tak pozostaje pytanie jakie błedy będziemy starali się wykrywać.
Na to pytanie nie ma jednoznacznej odpowiedzi, jak zresztą na
większość pytań dotyczących decyzji projektowych. Różne projekty
wymagają różnego poziomu niezawodności, a więc i różnych zabezpieczeń.
Na jednym końcu są programy, które po prostu nie mogą "paść", na
drugim np. niektóre moje progamy symulacyjne, które wykonują się w
godzinę lub mniej.
Nawet jednak w tej ostatniej sytuacji, różne formy obsługi błędów
mogą  nam bardzo pomóc w debugowaniu. Linijki typu:
  if( NULL<nowiki>=</nowiki><nowiki>=</nowiki>(fin<nowiki>=</nowiki>fopen(input_file_name,"r"))) {
    fprintf(stderr,"cannot open file input_file_name);
    exit(1); }
}
lub
  if( NULL<nowiki>=</nowiki><nowiki>=</nowiki>(p<nowiki>=</nowiki>malloc(n_bytes))) {
    fprintf(stderr,"cannot allocate memory for ...");
    exit(1);
  }
}
mogą nam oszczedzić, jeśli nie godzin, to wielu minut frustracji.
Proszę zwrócić uwagę że oba przykłady dotyczą  zasobów
zewnętrznych, nie do końca pod naszą kontrolą i tym bardziej powinny
być sprawdzane, zwłaszcza, że to będzie nas kosztować minutę pisania
i prawie tyle co nic w trakcie wykonania.
===Kontrola zakresu===
A co z bardziej kosztownymi testami? Typowym przkładem jest
sprawdzanie zakresu. Czy np. nasz stos <code>Stack</code> powinien
sprawdzać, czy wykonujemy <code>pop</code> lub <code>top</code> na stosie pustym,
albo <code>push</code> na stosie pełnym? Czy pwonniśmy sprawdzać
poprawność podanego indeksu w wyrażeniach <code>v[i]</code>?
Mam nadzieję że państwo nie oczekują jednoznacznej odpowiedzi na te
pytania, bo jej poprostu nie ma. Niestety tego rodzaju testy mogą być
bardzo kosztowne. Operacje dostępu do elementów są bardzo proste,
koszt testu będzie pewnie dominujący, a te operacje mogą być bardzo
często wykonywane. Z drugiej strony  błedy przekroczenia zakresu
są bardzo "wredne". Rozważmy np. taki prosty kod:
  Stack<int,5> s;
 
  for(int i<nowiki>=</nowiki>0;i<1000;i++)
    s.push(i);
  int i<nowiki>=</nowiki>0;
  while(1)
    std::cerr<<++i<<" "<<s.pop()<<"";
/* {mod13/code/overflow.cpp}{overflow.cpp} */
Na moim komputerze powyższy program, wykonał 20981 operacji <code>pop()</code>,
zanim padł z komunikatem <tt>Naruszenie ochrony pamięci</tt>.  Proszę
zauważyć że najpierw zapisał 995 liczb w pamięci należacej nie wiadomo
do kogo! Skutki takiego błedu mogą więc wystapić w zupełnie innym
miejscu programu.
Nie ma dobrego rozwiązania tego dylematu, ale zawsze może pomóc zdrowy
rozsądek. Użycie sprawdzenia zakresu w operacji mnożenia macierzy,
miałoby katastrofalne skutki dla wydajności kodu. Z drugiej strony
jest to prosty kod w którym łatwo zapewnić aby indeksy nie wychodziły
poza zakres. Tutaj więc kontrola zakresu jest niewskazana.
Częstym rozwiązaniem jest właczanie kontroli zakresu podczas
debugowania i wyłączanie jej w "produkcyjnej" wersji programu.  Moim
zdaniem, może to być bardzo pożyteczne, zwłaszcza w językach które
dopuszczają właczanie i wyłączanie sprawdzania zakresów za pomocą
opcji kompilacji (oczywiście może to dotyczyć tylko typów
wbudowanych). W przypadku stosu, mogą  nam się przydać w tym celu
klasy wytyczne, opracowane w rozdziale [[##lbl:wytyczne|Uzupelnic lbl:wytyczne|]]. Może warto
dodać, że kontenery STL, dostarczające operację indeksowania,
dostarczają również metodę dostępu ze sprawdzaniem: <code>at(int)</code>.
==Obsługa błędów==
Załóżmy więc, że w pogramie mamy przynajmniej kilka linijek
wykrywających potencjalne błedy.  No i stało się, wiemy już, że w
programie wystąpił bład, co teraz? Mamy wiele możliwości, wymienię
tylko kilka z nich:
# Kończymy program z ewentualnym komunikatem o błędzie.
# Kończymy program, ale najpierw sprzątamy po sobie: zwalniamy
  zasoby, których nie zwolni system operacyjny, zapisujemy dane, itp.
# Staramy się kontynuować program pomimo błądu, próbując go
  poprawić, obejść lub zrezygnować z części funkcjonaności.
W tym wykładzie nie będzi interesować nas sama konkretna strategia,
ale sposób rozdzielenia procesu wykrycia błędu od wyboru strategii.
Jest to problem który dotyczy każdego kodu, ale głównie funkcji
bibliotecznych.  Ich projektant/programista może wykryć, że w trakcie
ich wykonywania wystąpiła nieprawidłowość, ale nie może jednak wiedzieć
jak z takim błędem postąpić.  To jest decyzja osoby korzystającej z
tej funkcji. Musi więc istnieć jakiś mechanizm przekazywania tej
informacji z wywoływanej funkcji na zewnątrz do funkcji wywołującej.
Najprostyszym sposobem jest zwrócenie jakiejś wartości sygnalizującej
błąd. Jeśli funkcja nie zwraca żadnego wyniku, to jest to proste,
jeśli jednak funkcja ma zwracać jakiś wynik, to nie zawsze da się
znaleźć taką wartość, która by jednoznacznie mogła definiować błąd.
Rozszerzenia tej metody, to zwrócenie informacji o przebiegu funkcji
porzez dodatkowy argument przekazywany przez referencje.  Można też
ustawiać i odczytywać jakieś zmienne stanu.  Największą wadą tego
podejścia jest konieczność każdorazowego sprawdzania tych wartości, co
wymaga pisania dużej ilości trywialnego kodu. Z tego powodu
sprawdzania poprawności wywołania takich funkcji jest często
opuszczane. W C++ dochodzi jeszcze niemożność zwrócenia wartości z
konstruktora (choć oczywiście możemy ustawić w nim zmienną stanu
informująco o powodzeniu konstrukcji).
==Wyjatki==
C++ dostarcza nowego mechanizmu, są to wyjątki. Polega on na tym, że
funkcja która bład wykryje i nie chce lub nie może go obsłużyć sama,
rzuca wyjątek, który może być dowolnym obiektem.  Rzucenie wyjątku
powoduje natychmiastowe przerwanie wykonywania funkcji.  Procedura
wywołująca może ten wyjątek złapać. Wyjątek niezłapany prowadzi do
zatrzymana programu, a więc wyjątki nie mogą zostać zignorowane.
Zilustruję to na przykładzie naszego stosu, do którego dodam
instrukcje rzucające wyjątki, w przypadku przekroczenia zakresu
(dla prostoty nie będę korzystał z klas wytycznych):
 
template<typename T <nowiki>=</nowiki> int , size_t N <nowiki>=</nowiki> 100> class Stack {
private:
  T _rep[N];
  size_t _top;
public:
  Stack():_top(0) {};
  void push(T val) {
    if(_top <nowiki>=</nowiki><nowiki>=</nowiki> N) {
      throw "pushing on top of the full stack";
    }
    _rep[_top++]<nowiki>=</nowiki>val;
  }
  T pop() {
    if(is_empty()) {
      throw "poping form an empty stack";
    }
    return _rep[--_top];
  }
  bool is_empty() const    {return (_top<nowiki>=</nowiki><nowiki>=</nowiki>0);}
};
/* {mode13/code/stack_except.h}{stackexcept.h}*/
Polecenie <code>throw</code> służy właśnie do rzucania wyjątków. W tym wypadku
rzucane są stałe napisowe, które będą automatycznie konwertowane na typ
<code>const char *</code>. 
Wykonanie programu
main() {
  Stack<int,5> s;
  s.push(1);
  s.pop();
  s.pop(); /* tu będzie rzucony wyjątek */
 
  for(int i<nowiki>=</nowiki>0;i<10;i++)
  s.push(i); /* tu też gdyby udało się tu dojść */
}
/* {mod13/code/overflow.cpp}{overflow.cpp} */ 
spowoduje przerwanie programu w trakcie wykonywania drugiego polecenia
<code>pop</code>. Komunikat, który się przy tym pojawia jest zależny
od implementacji.
Wyjątki można łapać korzystając z bloku <code>try</code>.
  Stack<int,5> s;
  s.push(1);
  try {
    s.pop();
    s.pop();
  }
  catch(const char *msg) {
    std::cerr<<msg<<std::endl;
  }
  try {
      for(int i<nowiki>=</nowiki>0;i<10;i++)
    s.push(i);
  }
  catch(const char *msg) {
    std::cerr<<msg<<std::endl;
  }
/* {mod13/code/stack_except.cpp}{stackexcept.cpp} */
W bloku <code>try</code> umieszczamy instrukcje które mogą potencjalnie rzucić
wyjątek. Za blokiem <code>try</code> umieszczamy jedną lub więcej klauzul <code>catch</code>
które te wyjątki łapią. Wyjątek rzucony w bloku <code>try</code>  powoduje przekazanie
sterowania do pierwszej pasującej klauzuli <code>catch</code>.
==Wyjatki złapane==
Przyjrzyjmy się teraz dokladniej mechanizmowi rzucania i łapania
wyjątków.  Rozważmy prosty przykład:
struct X {
  int val;
  X(int i<nowiki>=</nowiki>0):val(i) {cerr<<"constructing "<<val<<"";}
  &nbsp;X() {cerr<<"destructing "<<val<<endl;} 
};
void f()  {
  X x(1);
  throw 0;
  cout<<"f";
};
main(){
  X  y(2);
  try {
    X z(3);
    f();
    cout<<"try";
  }
  catch(double){cout<<"zlapalem double-a";}
  catch(int){cout<<"zlapalem int-a";}
  catch(...){cout<<"zlapalem cos ";}
cout<<"main";
}
/* {mod13/code/caught.cpp}{caught.cpp} */
Oto wynik wykonania tego programu:
constructing 2
constructing 3
constructing 1
destructing 1
destructing 3
zlapalem int-a
main
destructing 2
Co możemy zauważyć?
# Wyjątek przerwał wykonywanie funkcji <code>f()</code> i bloku <code>try</code>,
sterowanie zostało przekazana do klauzuli <code>catch(int)</code>.
# Przedtem  wywołane zostały destruktory obiektów <code>x</code> i <code>z</code>, czyli
lokalnych obiektów w zasięgu bloku <code>try</code>. Ten proces nazywamy "zwijaniem stosu".
# Po wykonaniu klauzuli <code>catch</code> sterowanie zostało przekazane
  do następnego wyrażenia.
Klauzula <code>catch(...)</code> wyłapuje każdy wyjatek, jeśli np. pominiemy
klauzule <code>catch(int)</code>:
catch(double){cout<<"zlapalem double-a";}
//catch(int){cout<<"zlapalem int-a";}
catch(...){cout<<"zlapalem cos ";}
to wynikiem wywołania programu będzie:
 
constructing 2
constructing 3
constructing 1
destructing 1
destructing 3
zlapalem cos
main
destructing 2
Z tego przykładu widać też, że w przypadku dopasowywania klauzul
<code>catch</code> nie następuje niejawna konwersja argumentów.
==Nie złapane wyjątki==
A co się stanie, jeśli wyjątku nie złapiemy ? Zeby się o tym przekonać
usuniemy kolejną klauzulę <code>catch</code>:
catch(double){cout<<"zlapalem double-a";}
//catch(int){cout<<"zlapalem int-a";}
//catch(...){cout<<"zlapalem cos ";}
 
Wynik programu jest teraz zupełnie inny:
 
constructing 2
constructing 3
constructing 1
terminate called after throwing an instance of 'int'
Abort
Niezłapany wyjątek spowodował wywołanie funkcji <code>abort()</code>, która
zakończyła program bez wywołania  destruktorów. Sciśle rzecz
biorąc, niezłapany wyjątek wywołuje funkcję <code>terminate()</code>, która z
kolei domyślnie wywołuje funkcję <code>abort()</code>. Co do tego, czy wywoływane
są destruktory lokalnych obiektów (zwijanie stosu), to jest to
zachowanie zależne od implementacji.  Jak widać, w implementacji
<code>g++</code>, w przypadku niezłapania wyjątku destruktory obiektów nie są
wywoływane.
Domyślne zachowanie funkcji <code>terminate()</code> można zmienić, ustawiając
własną funkcje, za pomocą:
namespace std {
typedef void (*terminate_handler)(void);
terminate_handler set_terminate(terminate_handler new_terminate);
}
Funkcja ustawiana w tym poleceniu nie może zwrócić sterowania, taka
próba kończy sie wywołaniem funkcji <code>abort()</code>. Oznacza to, że funkcja
<code>new_terminate()</code> musi kończyć się wywołaniem <code>abort()</code> lub
<code>exit()</code>.
 
void my_terminate() {std::cerr<<"terminating "<<std::endl;exit(1);}
main() {
  std::set_terminate(my_terminate);
  throw 0;
}
/* {mod1/code/terminate.cpp}{terminate.cpp}*/
==Wyjątki w destruktorach==
Jeśli podczas opisanego powyżej procesu obsługi wyjątku, wywołana
zostanie funkcja która sama wywoła wyjątek, to program zostanie
natychmiast przerwany wykonaniem funkcji <code>terminate()</code> (nie dotyczy
to już funkcji wywoływanych wewnątrz klauzuli <code>catch</code>).  W
szczególności stanie się to jeśli któryś z destruktorów wywoływanych w
trakcie zwijania stosu rzuci wyjątek:
 
struct X {
  &nbsp;X() {
    std::cerr<<std::uncaught_exception()<<"";
    throw 0;};
} ;
main() {
  try
    {
      X x;
    }
  catch(int) {};
 
  try {
    X x;
    throw 0;
  }
  catch(int) {};
}
/* {mode13/code/destruktor.cpp}{destruktor.cpp} */
W powyższym kodzie destruktor klasy <code>X</code> jest wołany dwa razy. Po raz
pierwszy, podczas wychodzenia z pierwszego bloku <code>try</code>. Jest to
normalne wywołanie spowodowane wyjściem poza zakres.
Destruktor rzuca wyjątek, który zostaje wyłapany poprzez
klauzulę <code>catch(int)</code> na końcu bloku. Drugi raz, destruktor jest
wołany jako część zwijania stosu po wyjątku rzuconym jawnie w drugim bloku
<code>try</code>. Mimo, że łapiemy wyjątki <code>int</code>, i tak w tej sytuacji
wywoływana jest funkcja <code>terminate()</code>, a w konsekwencji i
<code>abort()</code>. Jest to  jeden z powodów, dla których destruktory
nie powinny rzucać wyjątków. Funkcja <code>uncaught_exception()</code>
umożliwia rozróżnienie tych dwu kontekstów wywołania destruktora.
Zwraca ona prawdę jeśli jakiś wyjątek jest właśnie obsługiwany.
Inne powody nie rzucania wyjątków z destruktorow, wiążą się z dynamicznym
alokacja pamięci  i zostaną omówione w kolejnym  wykładzie.
==Hierachie wyjątków==
Jako wyjątek może zostać wyrzucony dowolny obiekt. Umożliwia  nam to
grupowanie wyjątków w hierarchie, za pomocą dziedziczenia. Zilustrujemy
to za pomocą hierachii wyjątków z biblioteki standardowej, przedstawionej
na rysunku&nbsp;[[##fig:exceptions|Uzupelnic fig:exceptions|]].
[p]
 
{Hierarchia wyjątków biblioteki standardowej.}
Można z niej korzystać np. następująco:
 
main() {
  try {
        throw domain_error() ;
  }
  catch(invalid_argument &e) {
    cerr<<e.what()<<"";
  }
  catch(logic_error &e) {
    cerr<<"logic "<<e.what()<<"";
  }
  catch(exception &e) {
    cerr<<"some exception "<<e.what()<<"";
  }
}
/* {mod13/code/hierarchy.cpp}{hierarchy.cpp}
Kolejność klauzul <code>catch</code> jest ważna, ponieważ klauzule są
sprawdzane po kolei, i pierwsza która pasuje zostanie wykonana.
Gdybyśmy więc podali kaluzulę <code>catch(Exception &e)</code> jako pierwszą,
przechwyciła by ona wszyskie standardowe wyjątki. Ważne jest też, aby
korzystając z hierarchii dziedziczenia przechwytywać wyjątki przez
referencję.  Inaczej nie zostaną wywołane poprawne funkcje wirtualne.
Zachęcam do eksperymentów z powyższym kodem.
==Deklaracje wyjatków==
C++ pozwala na deklarowanie listy możliwych wyjątków rzucanych z
funkcji.  Służy do tego deklaracja <code>throw(...)</code> umieszana za
deklaracją listy argumentów funkcji np.:
void no_throw(int) throw();
 
deklaruje, że funkcja <code>no_throw</code> nie rzuci żadnego wyjątku, natomiast
void throw_std(int) throw(exception);
 
deklaruje, że funkcja <code>throw_std</code> będzie rzucać tylko wyjątki ze
standardowej hierarchi. Brak deklaracji oznacza, że funkcja może rzucać
co chce.
Niestety, C++ nie dostarcza nam praktycznie żadnych mechanizmów, które
by mogły wymusić konsystencję tych deklaracji. Rozważmy implementację
funkcji:
void f(int i) {throw i;}
void g() throw(int) {throw 0;};
void no_throw(int i) {f(i;};
Funkcja <code>f</code> proklamuje całemu światu, że może rzucać wyjątek
(poprzez brak deklaracji, że nie może), podobnie funkcja <code>g()</code>,
a pomimo to kompilator nie
pozwala na to, aby wywołać ją wewnątrz funkcji która jawnie deklaruje,
że wyjątku nie rzuci. Proszę to porównać np. ze zastosowaniem
kwalifikatora <code>const</code>: funkcja zadeklarowana jako <code>const</code> nie
może wołać funkcji, które <code>const</code> nie są. W przypadku wyjątków
narzucenie takiej konsystencji spowodowałoby to, że potrzeba by
przerabiać ogromne ilości kodu, napisanego zanim mechanizm wyjątków
stał się używany. Sprawia to niestety, że deklaracje wyjątków nie są
zbyt użyteczne, łatwo bowiem niechcący napisać kod który je złamie.  A
konsekwencje tego są poważne. Sprawdzania konsystencji w czasie
kompilacji wprawdzie nie ma, ale jest sprawdzanie w czasie wykonania.
Jeżeli funkcja rzuci wyjątek, który nie znajduje się na jej liście
zadeklarowanych wyjątków, to następuje wywołanie funkcji
<code>unexpected()</code>, która domyślnie wywołuje <code>abort()</code>. Nawet
złapanie tego wyjątku nic nie pomoże.
Kolejnym problem są szablony funkcji i metody szablonów klas. Ich
projektant nie może przewidzieć z jakimi argumentami zostaną one
konkretyzowane, a więc jakie wyjątki mogą zostać rzucone. Dlatego w
szablonach lepiej deklaracje wyjątków pomijać. W ogóle, deklaracje
wyjątków należy umieszczać tam, gdzie uważamy, że rzucenie każdego innego
wyjątku jest przejawem poważnego błedu. Najczęściej jest to sytuacja,
kiedy chcemy zadeklarować, że dana funkcja w ogóle nie rzuca wyjątków.
Jak już pisałem taką własność powinny posiadać destruktory.
===Niespodziewane wyjątki===
Podobnie jak w przypadku funkcji <code>terminate()</code> możemy podstawić
własną funkcję <code>unexpected()</code>. Podobnie jak <code>terminate()</code> funkcja
<code>unexpected()</code> nie zwraca sterowania, może za to sama rzucić
wyjątek.  W ten sposób możemy jej użyć do "podmiany"
niespodziewanego wyjątku na inny. Zobaczmy jak to działa:
 
void f() throw(int) {
  throw "niespodzianka!";
};
main() {
  try {
  f();
  }
  catch(...) {};
}
/* {mod13/code/unexpected.cpp}{unexpected.cpp} */
Ponieważ <code>f()</code>  rzuca <code>const char *</code>, a deklaruje tylko <code>int</code>-a,
powyższy program wywoła funkcję <code>unexpected()</code>, która przerwie program.
Jeżeli podmienimy wyjątek poprzez ustawienie odpowiedniej funkcji:
void unexpected_handler() {throw 0;};
std::set_unexpected(unexpected_handler);
 
to zamiast <code>const char *</code> zostanie rzucony <code>int</code> i złapany przez
klauzulę <code>catch(...)</code>. Tak się stanie ponieważ <code>int</code> jest na
liście wyjątków funkcji <code>f()</code>. Gdyby nie był, to zostałaby wywołana
funkcja <code>terminate()</code>. Jeżeli jednak deklaracja wyjątków funkcji
<code>f()</code> zawierać będzie wyjatek <code>std::bad_exception</code>, to każdy
wyjątek rzucony przez <code>unexpected()</code> i nie znajdujący się na liście
zadeklarowanych wyjątków funkcji <code>f()</code> jest podmienieny na
<code>std::bad_exception()</code>:
void f() throw(int,std::bad_exception) {
  throw "niespodzianka!";
};
void unexpected_handler() {throw 3.1415926;};
main() {
  std::set_unexpected(unexpected_handler);
  try {
  f();
  }
  catch(std::bad_exception ) {};
}
/* {mod13/code/unexpected.cpp}{unexpected.cpp} */
==Wydajność wyjątków==
Autor pozycji {MeyersMECPP} sugerował, że użycie mechanizmu
wyjątków może spowolnić program, nawet jeśli wyjątki nie będą rzucane.
Ponieważ od tego czasu minęło dobrych kilka lat, postanowiłem sam
sprawdzić jak się sprawy mają. W tym celu skorzystałem z następujących
programików:
 
double scin(double x,bool flag) {
  if(flag) throw 0;
  return sin(x);
}
main(){
  volatile int f<nowiki>=</nowiki>0;
  double s<nowiki>=</nowiki>0.0;
  for(int i<nowiki>=</nowiki>0;i<100000000;++i) {
    try {
    s+<nowiki>=</nowiki>scin(rand()/(double)RAND_MAX,f);
    } catch(int) {};
  }
}
/* {mod13/code/exceptions.cpp}{exceptions.cpp} */
oraz
 
double scin(double x,bool flag) {
  if(flag) return 0;
  return sin(x);
}
main(){
  volatile int f<nowiki>=</nowiki>0;
  double s<nowiki>=</nowiki>0.0;
  for(int i<nowiki>=</nowiki>0;i<100000000;++i)
    s+<nowiki>=</nowiki>scin(rand()/(double)RAND_MAX,f);
}
/* {mod13/code/no_exceptions.cpp}{noexceptions.cpp} */
Jak widać drugi z nich nie ma nawet śladu wyjątków. W pierwszym podejściu
ustawilem flagę <code>f</code> na zero, co powodowało że żaden wyjątek nie był rzucany.
Czas wykonania obu programów (w sekundach) podany jest w poniższej tabelce.
 
{| border=1
|+ <span style="font-variant:small-caps">Uzupelnij tytul</span>
|-
|
||  1  ||  2  ||  3  ||  4
|-
|
-O0  ||  15  ||  15  ||    || 
|-
| -O1  ||  13  ||  4  ||    ||
|-
| -O2  ||  13  ||  4  ||    ||
|-
| -O3  ||  4  ||  4  ||  600 ||  4
|-
|
{5}{l}{1 nie rzucane wyjątki}
|-
| {5}{l}{2 bez wyjątków}
|-
| {5}{l}{3 rzucane wyjątki}
|-
| {5}{l}{4 <tt>return</tt>}
|}
Porównując kolumny 1 i 2 widać że dla pełnej optymalizacji, nie ma
żadnej różnicy. Szczegółowe badanie wykazało, że to włączenie opcji
<code>-finline-functions</code> powoduje skok predkości pomiędzy dwoma
ostatnimi wierszami w pierwszej kolumnie. Ten sam effekt można uzyskać
dodając do funkcji <code>scin</code> kwalifikator <code>inline</code>.
Następnie porównałem koszt zwykłego powrotu z funkcji, z kosztem
rzucenia wyjątku. Wyniki są przedstawione w dwóch ostatnich kolumnach
tabelki.  Tu widać dramatyczną różnicę: obsługa wyjątku jest ponad 100
razy wolniejsza.
==Kiedy rzucać wyjątki==


==Podsumowanie==
==Podsumowanie==

Wersja z 15:48, 30 sie 2006

Uwaga: przekonwertowane latex2mediawiki; prawdopodobnie trzeba wprowadzić poprawki

Wstęp

Jesteśmy istotami omylnymi, więc niezależnie od naszych starań nasze programy bedą zawierały usterki, dostarczane do nich dane nie zawsze będą poprawne, a i sprzęt nie zawsze będzie działał tak jak trzeba.

Nie oznacza to, że należy zaniechać dążenia do pisania bezbłednego kodu, wprost przeciwnie, jakość kodu powinna być jednym z naszych piorytetów, ale należy się też pogodzić z faktem, że błędy bedą występować i powinniśmy być na takie sytuacje przygotowani, jak to mówią "najwyższą formą zaufania jest kontrola".

Na potrzeby tego wykładu zdefiniujemy bardzo luźno błąd jako wystąpienie sytuacji, która wystąpić nie powinna. Nie będziemy też interesować się bardzo tym jak błedy wykrywać, ale raczej co zrobić kiedy takowy wykryjemy. W następnym podrozdziale omówię bardzo pobieżnie różne możliwości reakcji na wystąpienie błedu i wprowadzę pojęcie wyjątku. Reszta wykładu będzie poświęcona zagadnieniom związanym z pisaniem kodu używającego wyjątki.

Wykrywanie błedów

Jak już napisałem we wstępie chciąłbym od razu przejść do sytuacji, w której wiemy że wystapił błąd. Jednak muszę poświęcić najpierw kilka akapitów na zastanowienie się, czy w ogóle należy będy wykrywać i obsługiwać. Nawet jeśli większość z państwa krzyknie "oczywiście, że tak" (choć podejrzewam że większość tego sama nie robi: kto ostatnio sprawdzał wartość zwróconą przez funkcję printf?), to i tak pozostaje pytanie jakie błedy będziemy starali się wykrywać.

Na to pytanie nie ma jednoznacznej odpowiedzi, jak zresztą na większość pytań dotyczących decyzji projektowych. Różne projekty wymagają różnego poziomu niezawodności, a więc i różnych zabezpieczeń.

Na jednym końcu są programy, które po prostu nie mogą "paść", na drugim np. niektóre moje progamy symulacyjne, które wykonują się w godzinę lub mniej. Nawet jednak w tej ostatniej sytuacji, różne formy obsługi błędów mogą nam bardzo pomóc w debugowaniu. Linijki typu:

if( NULL==(fin=fopen(input_file_name,"r"))) { 
    fprintf(stderr,"cannot open file input_file_name);
    exit(1); }
}

lub

if( NULL==(p=malloc(n_bytes))) { 
    fprintf(stderr,"cannot allocate memory for ...");
    exit(1); 
  }
}

mogą nam oszczedzić, jeśli nie godzin, to wielu minut frustracji. Proszę zwrócić uwagę że oba przykłady dotyczą zasobów zewnętrznych, nie do końca pod naszą kontrolą i tym bardziej powinny być sprawdzane, zwłaszcza, że to będzie nas kosztować minutę pisania i prawie tyle co nic w trakcie wykonania.

Kontrola zakresu

A co z bardziej kosztownymi testami? Typowym przkładem jest sprawdzanie zakresu. Czy np. nasz stos Stack powinien sprawdzać, czy wykonujemy pop lub top na stosie pustym, albo push na stosie pełnym? Czy pwonniśmy sprawdzać poprawność podanego indeksu w wyrażeniach v[i]?

Mam nadzieję że państwo nie oczekują jednoznacznej odpowiedzi na te pytania, bo jej poprostu nie ma. Niestety tego rodzaju testy mogą być bardzo kosztowne. Operacje dostępu do elementów są bardzo proste, koszt testu będzie pewnie dominujący, a te operacje mogą być bardzo często wykonywane. Z drugiej strony błedy przekroczenia zakresu są bardzo "wredne". Rozważmy np. taki prosty kod:

Stack<int,5> s;
  
  for(int i=0;i<1000;i++)
    s.push(i);

  int i=0;
  while(1) 
    std::cerr<<++i<<" "<<s.pop()<<"";
/* {mod13/code/overflow.cpp}{overflow.cpp} */

Na moim komputerze powyższy program, wykonał 20981 operacji pop(), zanim padł z komunikatem Naruszenie ochrony pamięci. Proszę zauważyć że najpierw zapisał 995 liczb w pamięci należacej nie wiadomo do kogo! Skutki takiego błedu mogą więc wystapić w zupełnie innym miejscu programu.

Nie ma dobrego rozwiązania tego dylematu, ale zawsze może pomóc zdrowy rozsądek. Użycie sprawdzenia zakresu w operacji mnożenia macierzy, miałoby katastrofalne skutki dla wydajności kodu. Z drugiej strony jest to prosty kod w którym łatwo zapewnić aby indeksy nie wychodziły poza zakres. Tutaj więc kontrola zakresu jest niewskazana.

Częstym rozwiązaniem jest właczanie kontroli zakresu podczas debugowania i wyłączanie jej w "produkcyjnej" wersji programu. Moim zdaniem, może to być bardzo pożyteczne, zwłaszcza w językach które dopuszczają właczanie i wyłączanie sprawdzania zakresów za pomocą opcji kompilacji (oczywiście może to dotyczyć tylko typów wbudowanych). W przypadku stosu, mogą nam się przydać w tym celu klasy wytyczne, opracowane w rozdziale Uzupelnic lbl:wytyczne|. Może warto dodać, że kontenery STL, dostarczające operację indeksowania, dostarczają również metodę dostępu ze sprawdzaniem: at(int).

Obsługa błędów

Załóżmy więc, że w pogramie mamy przynajmniej kilka linijek wykrywających potencjalne błedy. No i stało się, wiemy już, że w programie wystąpił bład, co teraz? Mamy wiele możliwości, wymienię tylko kilka z nich:

  1. Kończymy program z ewentualnym komunikatem o błędzie.
  2. Kończymy program, ale najpierw sprzątamy po sobie: zwalniamy zasoby, których nie zwolni system operacyjny, zapisujemy dane, itp.
  3. Staramy się kontynuować program pomimo błądu, próbując go poprawić, obejść lub zrezygnować z części funkcjonaności.

W tym wykładzie nie będzi interesować nas sama konkretna strategia, ale sposób rozdzielenia procesu wykrycia błędu od wyboru strategii. Jest to problem który dotyczy każdego kodu, ale głównie funkcji bibliotecznych. Ich projektant/programista może wykryć, że w trakcie ich wykonywania wystąpiła nieprawidłowość, ale nie może jednak wiedzieć jak z takim błędem postąpić. To jest decyzja osoby korzystającej z tej funkcji. Musi więc istnieć jakiś mechanizm przekazywania tej informacji z wywoływanej funkcji na zewnątrz do funkcji wywołującej.

Najprostyszym sposobem jest zwrócenie jakiejś wartości sygnalizującej błąd. Jeśli funkcja nie zwraca żadnego wyniku, to jest to proste, jeśli jednak funkcja ma zwracać jakiś wynik, to nie zawsze da się znaleźć taką wartość, która by jednoznacznie mogła definiować błąd. Rozszerzenia tej metody, to zwrócenie informacji o przebiegu funkcji porzez dodatkowy argument przekazywany przez referencje. Można też ustawiać i odczytywać jakieś zmienne stanu. Największą wadą tego podejścia jest konieczność każdorazowego sprawdzania tych wartości, co wymaga pisania dużej ilości trywialnego kodu. Z tego powodu sprawdzania poprawności wywołania takich funkcji jest często opuszczane. W C++ dochodzi jeszcze niemożność zwrócenia wartości z konstruktora (choć oczywiście możemy ustawić w nim zmienną stanu informująco o powodzeniu konstrukcji).

Wyjatki

C++ dostarcza nowego mechanizmu, są to wyjątki. Polega on na tym, że funkcja która bład wykryje i nie chce lub nie może go obsłużyć sama, rzuca wyjątek, który może być dowolnym obiektem. Rzucenie wyjątku powoduje natychmiastowe przerwanie wykonywania funkcji. Procedura wywołująca może ten wyjątek złapać. Wyjątek niezłapany prowadzi do zatrzymana programu, a więc wyjątki nie mogą zostać zignorowane. Zilustruję to na przykładzie naszego stosu, do którego dodam instrukcje rzucające wyjątki, w przypadku przekroczenia zakresu (dla prostoty nie będę korzystał z klas wytycznych):

template<typename T = int , size_t N = 100> class Stack {
private:	
  T _rep[N];
  size_t _top;
public:
  Stack():_top(0) {};
  void push(T val) {
    if(_top == N) {
      throw "pushing on top of the full stack";
    }
    _rep[_top++]=val;
  }
  T pop() {
    if(is_empty()) {
      throw "poping form an empty stack";
    }
    return _rep[--_top];
  }
  bool is_empty() const     {return (_top==0);} 
};
/* {mode13/code/stack_except.h}{stackexcept.h}*/

Polecenie throw służy właśnie do rzucania wyjątków. W tym wypadku rzucane są stałe napisowe, które będą automatycznie konwertowane na typ const char *. Wykonanie programu

main() {
  Stack<int,5> s;
  s.push(1);
  s.pop(); 
  s.pop(); /* tu będzie rzucony wyjątek */
  
  for(int i=0;i<10;i++) 
  s.push(i); /* tu też gdyby udało się tu dojść */
}
/* {mod13/code/overflow.cpp}{overflow.cpp} */  

spowoduje przerwanie programu w trakcie wykonywania drugiego polecenia pop. Komunikat, który się przy tym pojawia jest zależny od implementacji.

Wyjątki można łapać korzystając z bloku try.

Stack<int,5> s;
  s.push(1);
  try {
    s.pop();
    s.pop();
  }
  catch(const char *msg) {
    std::cerr<<msg<<std::endl;
  }
  try {
      for(int i=0;i<10;i++) 
    s.push(i);
  }
  catch(const char *msg) {
    std::cerr<<msg<<std::endl;
  }
/* {mod13/code/stack_except.cpp}{stackexcept.cpp} */

W bloku try umieszczamy instrukcje które mogą potencjalnie rzucić wyjątek. Za blokiem try umieszczamy jedną lub więcej klauzul catch które te wyjątki łapią. Wyjątek rzucony w bloku try powoduje przekazanie sterowania do pierwszej pasującej klauzuli catch.

Wyjatki złapane

Przyjrzyjmy się teraz dokladniej mechanizmowi rzucania i łapania wyjątków. Rozważmy prosty przykład:

struct X {
  int val;
  X(int i=0):val(i) {cerr<<"constructing "<<val<<"";}
   X() {cerr<<"destructing "<<val<<endl;}  
};

void f()  {
  X x(1);
  throw 0;
  cout<<"f";
};

main(){
  X  y(2);
  try {
    X z(3);
    f();
    cout<<"try";
  } 
  catch(double){cout<<"zlapalem double-a";}
  catch(int){cout<<"zlapalem int-a";}
  catch(...){cout<<"zlapalem cos ";}

 cout<<"main";
}
/* {mod13/code/caught.cpp}{caught.cpp} */

Oto wynik wykonania tego programu:

constructing 2
constructing 3
constructing 1
destructing 1
destructing 3
zlapalem int-a
main
destructing 2

Co możemy zauważyć?

  1. Wyjątek przerwał wykonywanie funkcji f() i bloku try, sterowanie zostało przekazana do klauzuli catch(int).
  2. Przedtem wywołane zostały destruktory obiektów x i z, czyli lokalnych obiektów w zasięgu bloku try. Ten proces nazywamy "zwijaniem stosu".
  3. Po wykonaniu klauzuli catch sterowanie zostało przekazane do następnego wyrażenia.

Klauzula catch(...) wyłapuje każdy wyjatek, jeśli np. pominiemy klauzule catch(int):

catch(double){cout<<"zlapalem double-a";}
//catch(int){cout<<"zlapalem int-a";}
catch(...){cout<<"zlapalem cos ";}

to wynikiem wywołania programu będzie:

constructing 2
constructing 3
constructing 1
destructing 1
destructing 3
zlapalem cos
main
destructing 2

Z tego przykładu widać też, że w przypadku dopasowywania klauzul catch nie następuje niejawna konwersja argumentów.

Nie złapane wyjątki

A co się stanie, jeśli wyjątku nie złapiemy ? Zeby się o tym przekonać usuniemy kolejną klauzulę catch:

catch(double){cout<<"zlapalem double-a";}
//catch(int){cout<<"zlapalem int-a";}
//catch(...){cout<<"zlapalem cos ";}

Wynik programu jest teraz zupełnie inny:

constructing 2
constructing 3
constructing 1
terminate called after throwing an instance of 'int'
Abort

Niezłapany wyjątek spowodował wywołanie funkcji abort(), która zakończyła program bez wywołania destruktorów. Sciśle rzecz biorąc, niezłapany wyjątek wywołuje funkcję terminate(), która z kolei domyślnie wywołuje funkcję abort(). Co do tego, czy wywoływane są destruktory lokalnych obiektów (zwijanie stosu), to jest to zachowanie zależne od implementacji. Jak widać, w implementacji g++, w przypadku niezłapania wyjątku destruktory obiektów nie są wywoływane.

Domyślne zachowanie funkcji terminate() można zmienić, ustawiając własną funkcje, za pomocą:

namespace std {
typedef void (*terminate_handler)(void);
terminate_handler set_terminate(terminate_handler new_terminate);
}

Funkcja ustawiana w tym poleceniu nie może zwrócić sterowania, taka próba kończy sie wywołaniem funkcji abort(). Oznacza to, że funkcja new_terminate() musi kończyć się wywołaniem abort() lub exit().

void my_terminate() {std::cerr<<"terminating "<<std::endl;exit(1);}

main() {
  std::set_terminate(my_terminate);
  throw 0;
}
/* {mod1/code/terminate.cpp}{terminate.cpp}*/

Wyjątki w destruktorach

Jeśli podczas opisanego powyżej procesu obsługi wyjątku, wywołana zostanie funkcja która sama wywoła wyjątek, to program zostanie natychmiast przerwany wykonaniem funkcji terminate() (nie dotyczy to już funkcji wywoływanych wewnątrz klauzuli catch). W szczególności stanie się to jeśli któryś z destruktorów wywoływanych w trakcie zwijania stosu rzuci wyjątek:

struct X {
   X() {
    std::cerr<<std::uncaught_exception()<<"";
    throw 0;};
} ;

main() {
  try 
    {
      X x;
    }
  catch(int) {};
  
  try {
    X x;
    throw 0;
  }
  catch(int) {};
}
/* {mode13/code/destruktor.cpp}{destruktor.cpp} */

W powyższym kodzie destruktor klasy X jest wołany dwa razy. Po raz pierwszy, podczas wychodzenia z pierwszego bloku try. Jest to normalne wywołanie spowodowane wyjściem poza zakres. Destruktor rzuca wyjątek, który zostaje wyłapany poprzez klauzulę catch(int) na końcu bloku. Drugi raz, destruktor jest wołany jako część zwijania stosu po wyjątku rzuconym jawnie w drugim bloku try. Mimo, że łapiemy wyjątki int, i tak w tej sytuacji wywoływana jest funkcja terminate(), a w konsekwencji i abort(). Jest to jeden z powodów, dla których destruktory nie powinny rzucać wyjątków. Funkcja uncaught_exception() umożliwia rozróżnienie tych dwu kontekstów wywołania destruktora. Zwraca ona prawdę jeśli jakiś wyjątek jest właśnie obsługiwany.

Inne powody nie rzucania wyjątków z destruktorow, wiążą się z dynamicznym alokacja pamięci i zostaną omówione w kolejnym wykładzie.

Hierachie wyjątków

Jako wyjątek może zostać wyrzucony dowolny obiekt. Umożliwia nam to grupowanie wyjątków w hierarchie, za pomocą dziedziczenia. Zilustrujemy to za pomocą hierachii wyjątków z biblioteki standardowej, przedstawionej na rysunku Uzupelnic fig:exceptions|.

[p]
 

{Hierarchia wyjątków biblioteki standardowej.}

Można z niej korzystać np. następująco:

main() {
  try {
        throw domain_error() ;
  }
  catch(invalid_argument &e) {
    cerr<<e.what()<<"";
  }
  catch(logic_error &e) {
    cerr<<"logic "<<e.what()<<"";
  }
  catch(exception &e) {
    cerr<<"some exception "<<e.what()<<"";
  } 
}
/* {mod13/code/hierarchy.cpp}{hierarchy.cpp}

Kolejność klauzul catch jest ważna, ponieważ klauzule są sprawdzane po kolei, i pierwsza która pasuje zostanie wykonana. Gdybyśmy więc podali kaluzulę catch(Exception &e) jako pierwszą, przechwyciła by ona wszyskie standardowe wyjątki. Ważne jest też, aby korzystając z hierarchii dziedziczenia przechwytywać wyjątki przez referencję. Inaczej nie zostaną wywołane poprawne funkcje wirtualne. Zachęcam do eksperymentów z powyższym kodem.

Deklaracje wyjatków

C++ pozwala na deklarowanie listy możliwych wyjątków rzucanych z funkcji. Służy do tego deklaracja throw(...) umieszana za deklaracją listy argumentów funkcji np.:

void no_throw(int) throw();

deklaruje, że funkcja no_throw nie rzuci żadnego wyjątku, natomiast

void throw_std(int) throw(exception);

deklaruje, że funkcja throw_std będzie rzucać tylko wyjątki ze standardowej hierarchi. Brak deklaracji oznacza, że funkcja może rzucać co chce.

Niestety, C++ nie dostarcza nam praktycznie żadnych mechanizmów, które by mogły wymusić konsystencję tych deklaracji. Rozważmy implementację funkcji:

void f(int i) {throw i;}
void g() throw(int) {throw 0;}; 
void no_throw(int i) {f(i;};

Funkcja f proklamuje całemu światu, że może rzucać wyjątek (poprzez brak deklaracji, że nie może), podobnie funkcja g(), a pomimo to kompilator nie pozwala na to, aby wywołać ją wewnątrz funkcji która jawnie deklaruje, że wyjątku nie rzuci. Proszę to porównać np. ze zastosowaniem kwalifikatora const: funkcja zadeklarowana jako const nie może wołać funkcji, które const nie są. W przypadku wyjątków narzucenie takiej konsystencji spowodowałoby to, że potrzeba by przerabiać ogromne ilości kodu, napisanego zanim mechanizm wyjątków stał się używany. Sprawia to niestety, że deklaracje wyjątków nie są zbyt użyteczne, łatwo bowiem niechcący napisać kod który je złamie. A konsekwencje tego są poważne. Sprawdzania konsystencji w czasie kompilacji wprawdzie nie ma, ale jest sprawdzanie w czasie wykonania. Jeżeli funkcja rzuci wyjątek, który nie znajduje się na jej liście zadeklarowanych wyjątków, to następuje wywołanie funkcji unexpected(), która domyślnie wywołuje abort(). Nawet złapanie tego wyjątku nic nie pomoże.

Kolejnym problem są szablony funkcji i metody szablonów klas. Ich projektant nie może przewidzieć z jakimi argumentami zostaną one konkretyzowane, a więc jakie wyjątki mogą zostać rzucone. Dlatego w szablonach lepiej deklaracje wyjątków pomijać. W ogóle, deklaracje wyjątków należy umieszczać tam, gdzie uważamy, że rzucenie każdego innego wyjątku jest przejawem poważnego błedu. Najczęściej jest to sytuacja, kiedy chcemy zadeklarować, że dana funkcja w ogóle nie rzuca wyjątków. Jak już pisałem taką własność powinny posiadać destruktory.

Niespodziewane wyjątki

Podobnie jak w przypadku funkcji terminate() możemy podstawić własną funkcję unexpected(). Podobnie jak terminate() funkcja unexpected() nie zwraca sterowania, może za to sama rzucić wyjątek. W ten sposób możemy jej użyć do "podmiany" niespodziewanego wyjątku na inny. Zobaczmy jak to działa:

void f() throw(int) {
  throw "niespodzianka!";
};

main() {
  try {
  f();
  }
  catch(...) {};
}
/* {mod13/code/unexpected.cpp}{unexpected.cpp} */

Ponieważ f() rzuca const char *, a deklaruje tylko int-a, powyższy program wywoła funkcję unexpected(), która przerwie program. Jeżeli podmienimy wyjątek poprzez ustawienie odpowiedniej funkcji:

void unexpected_handler() {throw 0;};
std::set_unexpected(unexpected_handler);

to zamiast const char * zostanie rzucony int i złapany przez klauzulę catch(...). Tak się stanie ponieważ int jest na liście wyjątków funkcji f(). Gdyby nie był, to zostałaby wywołana funkcja terminate(). Jeżeli jednak deklaracja wyjątków funkcji f() zawierać będzie wyjatek std::bad_exception, to każdy wyjątek rzucony przez unexpected() i nie znajdujący się na liście zadeklarowanych wyjątków funkcji f() jest podmienieny na std::bad_exception():

void f() throw(int,std::bad_exception) {
  throw "niespodzianka!";
};

void unexpected_handler() {throw 3.1415926;};

main() {
  std::set_unexpected(unexpected_handler);
  try {
  f();
  }
  catch(std::bad_exception ) {};
}
/* {mod13/code/unexpected.cpp}{unexpected.cpp} */

Wydajność wyjątków

Autor pozycji {MeyersMECPP} sugerował, że użycie mechanizmu wyjątków może spowolnić program, nawet jeśli wyjątki nie będą rzucane. Ponieważ od tego czasu minęło dobrych kilka lat, postanowiłem sam sprawdzić jak się sprawy mają. W tym celu skorzystałem z następujących programików:

double scin(double x,bool flag) {
  if(flag) throw 0;
  return sin(x);
}
main(){
  volatile int f=0;
  double s=0.0;
  for(int i=0;i<100000000;++i) {
    try {
    s+=scin(rand()/(double)RAND_MAX,f);
    } catch(int) {};
  }
}
/* {mod13/code/exceptions.cpp}{exceptions.cpp} */

oraz

double scin(double x,bool flag) {
  if(flag) return 0;
  return sin(x);
}

main(){
  volatile int f=0;
  double s=0.0;
  for(int i=0;i<100000000;++i)
    s+=scin(rand()/(double)RAND_MAX,f);
}
/* {mod13/code/no_exceptions.cpp}{noexceptions.cpp} */

Jak widać drugi z nich nie ma nawet śladu wyjątków. W pierwszym podejściu ustawilem flagę f na zero, co powodowało że żaden wyjątek nie był rzucany. Czas wykonania obu programów (w sekundach) podany jest w poniższej tabelce.

Uzupelnij tytul
1 nie rzucane wyjątki 2 bez wyjątków 3 rzucane wyjątki 4 return
-O0 15 15
-O1 13 4
-O2 13 4
-O3 4 4 600 4

Porównując kolumny 1 i 2 widać że dla pełnej optymalizacji, nie ma żadnej różnicy. Szczegółowe badanie wykazało, że to włączenie opcji -finline-functions powoduje skok predkości pomiędzy dwoma ostatnimi wierszami w pierwszej kolumnie. Ten sam effekt można uzyskać dodając do funkcji scin kwalifikator inline.

Następnie porównałem koszt zwykłego powrotu z funkcji, z kosztem rzucenia wyjątku. Wyniki są przedstawione w dwóch ostatnich kolumnach tabelki. Tu widać dramatyczną różnicę: obsługa wyjątku jest ponad 100 razy wolniejsza.

Kiedy rzucać wyjątki

Podsumowanie