22.2 Wychwytywanie wyjątków

Do definiowania procedur obsługi wyjątków służą tzw. frazy catch. Mają one postać definicji funkcji, której parametr jest takiego typu, jakiego typu będzie obsługiwany przez tę frazę wyjątek. Każda fraza catch „zajmuje się” tylko wyjątkami określonego przez swój parametr typu. Każda też obsługuje wyłącznie wyjątki powstałe podczas wykonywania jednej instrukcji złożonej bezpośrednio poprzedzającej tę frazę — ta instrukcja złożona musi być poprzedzona słowem kluczowym try:

      1.      try {
      2.          // sekwencja instrukcji
      3.      }
      4.      catch(Typ t) {
      5.          // obsluga
      6.      }
      7.      // ...
W powyższym schematycznym przykładzie wyjątek pewnego typu Typ może zostać zgłoszony podczas wykonywania instrukcji złożonej oznaczonej słowem kluczowym try. Cała instrukcja złożona jest, jak zwykle, ujęta w nawiasy klamrowe i może zawierać dowolną liczbę instrukcji prostych i złożonych; mogą tam w szczególności występować wywołania funkcji. Jeśli wyjątek zostanie zgłoszony podczas wykonywania takiej funkcji, a nie został w niej obsłużony, to nastąpi powrót z niej i wyjątek będzie dalej obsługiwany tak, jakby został wysłany w miejscu, gdzie funkcja była wywołana.

Jeśli w trakcie wykonywania instrukcji złożonej z linii 1-3 wyjątek typu Typ (lub typu pochodnego od Typ) zostanie wysłany, to sterowanie przejdzie natychmiast do wykonywania instrukcji zawartych we frazie catch. Wewnątrz tej frazy dostępny będzie obiekt opisujący wyjątek (ten, który użyty został w odpowiedniej instrukcji throw). Składnia frazy catch przypomina składnię definicji funkcji. Również argument jest odbierany podobnie jak dla zwykłych funkcji. W szczególności można, i zazwyczaj tak się właśnie robi, odbierać go poprzez referencję. Możemy wtedy bowiem korzystać z dobrodziejstw polimorfizmu. Jeśli odbieramy ten argument przez wartość, to, jak dla funkcji, otrzymujemy kopię argumentu.

Czasem wystarczy nam informacja, że wyjątek danego typu został wysłany, a sam jego obiekt nie jest nam do niczego potrzebny i nie jest używany wewnątrz frazy catch. Wtedy w nagłówku frazy można pozostawić tylko określenie typu, a nazwę parametru pominąć.

Jeżeli wewnątrz procedury obsługi zawartej we frazie catch nie zostanie wysłany nowy wyjątek, nie nastąpi wykonanie instrukcji return lub zakończenie programu, to po jej wykonaniu sterowanie przejdzie do instrukcji następującej po frazie catch (linia 7 w naszym przykładzie).

Oczywiście do powstania sytuacji wyjątkowej podczas wykonywania bloku try nie musi dojść. Jeśli do niej nie dojdzie, to po zakończeniu wykonywania tego bloku sterowanie przejdzie za frazę catch (czyli znów do linii 7 w naszym przykładzie); sama fraza catch zostanie wtedy całkowicie pominięta.

Rozpatrzmy następujący program:


P171: excpt.cpp     Wyjątki jako obiekty klas polimorficznych

      1.  #include <iostream>
      2.  #include <string>
      3.  #include <sstream>   // ostringstream
      4.  #include <cmath>     // sqrt, log, atan
      5.  using namespace std;
      6.  
      7.  struct Blad {
      8.      virtual string opis() = 0;
      9.  };
     10.  
     11.  class Ujemna : public Blad {
     12.      double x;
     13.  public:
     14.      Ujemna(double x) : x(x) { }
     15.      string opis() {
     16.          ostringstream strum;
     17.          strum << "Ujemny argument: x = " << x;
     18.          return strum.str();
     19.      }
     20.  };
     21.  
     22.  class PozaZakresem : public Blad {
     23.      double x, min, max;
     24.  public:
     25.      PozaZakresem(double x, double mi, double ma)
     26.          : x(x), min(mi), max(ma)
     27.      { }
     28.      string opis() {
     29.          ostringstream strum;
     30.          strum << "Argument x = " << x << "\n    "
     31.                << "    poza zakresem [" << min << ","
     32.                << max << "]";
     33.          return strum.str();
     34.      }
     35.  };
     36.  
     37.  double fun(double x) {
     38.      if (x < 0 || x > 2) throw PozaZakresem(x,0,3);
     39.      return sqrt(x*(3-x));
     40.  }
     41.  
     42.  double logPI(double x) {
     43.      static double LPI = log(4*atan(1.));
     44.      if (x <= 0) throw Ujemna(x);
     45.      return log(x)/LPI;
     46.  }
     47.  
     48.  int main() {
     49.      double x = 4*atan(1.);
     50.  
     51.      cout << "x = " << x << endl;
     52.      try {
     53.          double z1 = logPI(x);
     54.          cout << "  z1 = " << z1 << endl;
     55.          double z2 = fun(x);
     56.          cout << "  z2 = " << z2 << endl;
     57.      }
     58.      catch(Blad& blad) {
     59.          cerr << "  Blad: " << blad.opis() << endl;
     60.      }
     61.  }

Definiujemy w tym programie klasę abstrakcyjną Blad i dwie klasy z niej dziedziczące: UjemnePozaZakresem. Obie implementują wirtualną funkcję opis odziedziczoną z klasy bazowej.

W liniach 37-46 definiujemy dwie funkcje globalne. Każda z nich może zgłosić wyjątek: funkcja fun wyjątek klasy PozaZakresem jeśli argumentem jest liczbą spoza zakresu [0, 3], a funkcja logPI wyjątek klasy Ujemne jeśli argument jest ujemny (funkcja oblicza logarytm argumentu przy podstawie π). W funkcji main wywołujemy w bloku try obie te funkcje; ewentualny błąd wychwytuje fraza catch zdefiniowana w liniach 58-60. Zauważmy, że przechwytuje ona wyjątki typu Blad przez referencję, a więc przechwyci wyjątki wszystkich klas pochodnych od Blad i będzie mogła korzystać z polimorfizmu. Tutaj tak właśnie jest, bo klasy są polimorficzne i funkcja opis jest wirtualna. Wywołanie tej metody w linii 59 spowoduje zatem wywołanie tej metody z prawdziwej klasy przekazanego poprzez argument obiektu,

    x = 3.14159
      z1 = 1
      Blad: Argument x = 3.14159
            poza zakresem [0,3]
a więc w naszym przypadku z klasy PozaZakresem, bo wyjątek został zgłoszony w funkcji fun.

T.R. Werner, 25 lutego 2017; 22:31