22.1 Zgłaszanie wyjątków

W dowolnym miejscu programu może być zgłoszony (wysłany) wyjątek (ang. throwing lub raising an exception). Często nawet nie wiemy, że funkcja biblioteczna, którą wywołujemy, może to zrobić. Lepiej jednak o tym wiedzieć. Autor takiej funkcji bibliotecznej nie znał naszego programu, więc nie mógł wiedzieć, jak obsłużyć daną sytuację wyjątkową. Aby nie przerywać programu, zgłasza więc wyjątek, a użytkownik korzystający z tej funkcji wiedząc, że taki wyjątek może zostać przez funkcję zgłoszony, sam definiuje sposób jego obsługi. Widzimy zatem, że mechanizm obsługi wyjątków daje możliwość pewnego rodzaju komunikacji między różnymi fragmentami kodu, być może pochodzącymi z różnych modułów i napisanych przez różnych autorów.

W funkcjach, które sami piszemy, możemy określić, kiedy i jaki wyjątek zostanie zgłoszony. Wyjątek może być opisany obiektem dowolnego typu, klasowego lub wbudowanego. Zazwyczaj tworzy się specjalne klasy, których obiekty będą opisywać wyjątki. Równie dobrze jednak wyjątek może być opisany po prostu liczbą lub napisem.

Wyjątek jest zgłaszany za pomocą operatora throw:

       throw excpt;
gdzie excpt może być obiektem dowolnego typu, również wbudowanego, jak int czy double. W momencie zgłoszenia wyjątku normalny przebieg programu jest przerywany i poszukiwana jest procedura obsługi danego wyjątku (czyli odpowiednia fraza catch, o czym powiemy w następnym podrozdziale). Jeśli taka procedura zostanie znaleziona, to wyjątek uważa się za obsłużony i wykonywana jest treść procedury. Nie ma automatycznego powrotu do miejsca zgłoszenia wyjątku!

Jeśli taka procedura nie zostanie znaleziona w funkcji, w której ta sytuacja wyjątkowa miała miejsce, to przepływ sterowania opuszcza kod funkcji, a ramka stosu związana z jej wywołaniem jest usuwana (stos jest „zwijany”). Oznacza to, między innymi, że zmienne lokalne zdefiniowane w tej funkcji są bezpowrotnie tracone. Dla usuwanych lokalnych zmiennych obiektowych wywoływane są, co bardzo ważne, ich destruktory. Jeśli funkcja była rezultatowa, to wartość zwracana jest nieokreślona i wobec tego bezużyteczna. Na tej samej zasadzie poszukiwanie procedury obsługi jest następnie kontynuowane w funkcji wywołującej. Jeśli i tam nie zostanie znaleziona, to i ta funkcja przerywa swoje działanie i jej ramka wywołania na stosie jest też zwijana. W ten sposób mamy dwie możliwości:

W poniższym programie podstawiamy funkcję termin zamiast domyślnej terminate (linia 18):


P173: term.cpp     Nieobsłużone wyjątki

      1.  #include <iostream>
      2.  #include <cmath>      // sqrt
      3.  #include <cstdlib>    // exit
      4.  #include <exception>
      5.  using namespace std;
      6.  
      7.  void termin() {
      8.      cout << "termin: exit(7)" << endl;
      9.      exit(7);
     10.  }
     11.  
     12.  double Sqrt(double x) {
     13.      if (x < 0) throw "x < 0";
     14.      return sqrt(x);
     15.  }
     16.  
     17.  int main() {
     18.      set_terminate(&termin);
     19.  
     20.      double z, x;
     21.  
     22.      x =  16;
     23.      z = Sqrt(x);
     24.      cout << "Sqrt(" << x << ")=" << z << endl;
     25.  
     26.      x = -16;
     27.      z = Sqrt(x);
     28.      cout << "Sqrt(" << x << ")=" << z << endl;
     29.  }

Z kolei w funkcji Sqrt zgłaszamy wyjątek, jeśli przekazany argument jest ujemny. Wyjątku tego nie przechwytujemy (czyli nie obsługujemy). Zatem wywołana zostanie funkcja termin. Nie ma ona prawa powrócić do funkcji wywołującej, ale powoduje, za pomocą wywołania funkcji exit (z pliku nagłówkowego cstdlib), zakończenie programu z tak zwanym statusem powrotu równym 7. Status ten jest odbierany przez powłokę systemu operacyjnego i może być w jakimś celu wykorzystany. W Linuksowych powłokach, jak tcsh czy bash, status powrotu ostatnio wykonanego programu staje się wartością wbudowanej zmiennej powłoki o nazwie '$?' i wobec tego jest znany tuż po zakończeniu programu, co często wykorzystuje się w skryptach powłoki
    cpp> g++ -pedantic-errors -Wall -o term term.cpp
    cpp> ./term
    Sqrt(16)=4
    termin: exit(7)
    cpp> echo $?
    7
W tym przykładzie każde powstanie wyjątku kończy się przerwaniem programu (tyle że w sposób cywilizowany). Zwykle jednak nie o to nam chodzi: chcielibyśmy przechwytywać wyjątki i sami decydować, czy i jak program ma być kontynuowany.

T.R. Werner, 23 lutego 2019; 23:59