22.4 Wyjątki w konstruktorach i destruktorach

Sytuacja wyjątkowa może zdarzyć się podczas wykonywania konstruktora lub destruktora. Taka sytuacja jest szczególnie trudna do właściwej obsługi. Powiedzmy zatem o kilku sprawach, o których trzeba wtedy pamiętać.

Jeśli wyjątek został zgłoszony podczas konstrukcji obiektu, to obiekt ten nie powstanie, jego destruktor nie zostanie wywołany, a wszystkie do tej pory utworzone składowe zostaną usunięte. Mogą to być już utworzone składowe obiektowe: dla nich destruktory zostaną wywołane. Oczywiście powstanie kłopot, jeśli są w klasie składowe wskaźnikowe, a same obiekty, na które one wskazują, zostały w konstruktorze zaalokowane na stercie lub odnoszą się do zasobów systemowych, jak np. plików. Tego typu obiekty są zwykle usuwane (zwalniane) w destruktorze, ale on nie zadziała. W ten sposób, w razie wystąpienia sytuacji wyjątkowej, nieudany obiekt zostanie co prawda usunięty, ale zasoby (pamięć, otwarte pliki) nie zostaną zwolnione. Można temu zaradzić „opakowując” tego rodzaju składowe wskaźnikowe tak, aby uczynić z nich obiekty, dla których w razie niepowodzenia wywołany zostanie destruktor zwalniający zasoby. Rozpatrzmy przykład:


P178: zasob.cpp     Zwalnianie zasobów w sytuacjach wyjątkowych

      1.  #include <iostream>
      2.  #include <cstring>
      3.  #include <cstdio>  // FILE, fopen, fclose
      4.  using namespace std;
      5.  
      6.  class A {
      7.      struct nazw {
      8.          char* n;
      9.          nazw(const char* n)
     10.              : n(strcpy(new char[strlen(n)+1],n))
     11.          { }
     12.          ~nazw() {
     13.              cerr << "dtor nazw: " << n << endl;
     14.              delete [] n;
     15.          }
     16.      };
     17.  
     18.      nazw Nazwisko;
     19.      FILE*    plik;
     20.  public:
     21.      A(const char* n, const char* p)
     22.          : Nazwisko(n)
     23.      {
     24.          plik = fopen(p,"r");
     25.          // ...
     26.  //      throw 1;
     27.          // ...
     28.      }
     29.  
     30.      // inne pola i metody
     31.  
     32.      ~A() {
     33.          cerr << "dtor A" << endl;
     34.          if (plik) fclose(plik);
     35.      }
     36.  };
     37.  
     38.  int main() {
     39.      try {
     40.          A a("Kowalski","zasob.cpp");
     41.      } catch(...) {
     42.          cerr << "Nie udalo sie skonstruowac obiektu\n";
     43.      }
     44.  }

Klasa  A zawiera składową opisującą nazwisko w postaci C-napisu. Sam napis alokowany jest dynamicznie, ale wskaźnik do niego nie jest bezpośrednio składową klasy  A. Zamiast tego składową tej klasy jest obiekt pomocniczej, „opakowującej” struktury nazw, który dopiero zawiera, jako swoją składową n, wskaźnik do napisu. Ta pomocnicza struktura definiuje destruktor usuwający napis ze sterty.

Prócz nazwiska, klasa  A zawiera pole wskaźnikowe wskazujące obiekt typu FILE (jest to standardowy typ w czystym C opisujący pliki).

Załóżmy, że linia 26 (throw 1) jest wykomentowana. Konstruktor klasy A inicjuje składowe opisujące nazwisko i kończy się prawidłowo. Żaden wyjątek nie został zgłoszony. Po wyjściu sterowania z ciała bloku try obiekt klasy  A, jako obiekt lokalny dla tego bloku, jest usuwany i wywoływany jest jego destruktor zamykający plik. Następnie usuwane są obiekty składowe i wywoływane są ich destruktory, a więc w naszym przypadku usunięty będzie obiekt Nazwisko, a w jego destruktorze zwolniona zostanie pamięć na nazwisko. Wydruk programu

    dtor A
    dtor nazw: Kowalski
świadczy o tym, że obiekt  a został prawidłowo usunięty.

Spróbujmy teraz uaktywnić linię 26, która powoduje powstanie sytuacji wyjątkowej w trakcie wykonywania konstruktora. Teraz wydruk z programu to

    dtor nazw: Kowalski
    Nie udalo sie skonstruowac obiektu
Po powstaniu wyjątku destruktor klasy  A dla powstającego obiektu nie został wywołany. Tak więc plik, choć już otwarty, nie został zamknięty — przepadł tylko wskaźnik do niego. Natomiast napis zawierający nazwisko został prawidłowo usunięty! Stało się tak, bo powstanie wyjątku spowodowało wywołanie destruktorów dla już utworzonych składowych obiektowych, a więc dla składowej Nazwisko.


W przykładzie powyższym nie wyłapywaliśmy wyjątku powstającego podczas konstruowania obiektu w samym konstruktorze, ale pozwoliliśmy mu wyjść poza konstruktor, gdzie był przechwytywany w funkcji main. Inna jest sytuacja z wyjątkami, jakie mogą powstać w trakcie wykonania destruktora. Problem polega na tym, że destruktor, jak mówiliśmy, może zostać wywołany podczas zwijania stosu w poszukiwaniu procedury obsługi innego wyjątku. Powstanie dodatkowego nieobsłużonego wyjątku w destruktorze powodowałoby „podwójne” zwijanie stosu. Taka sytuacja nie jest w C++ możliwa; jeśli powstanie, program jest natychmiast kończony za pomocą funkcji terminate. Tak więc, jeśli jakikolwiek wyjątek może być zgłoszony podczas wykonywania destruktora, to należy go obsłużyć — przechwycić odpowiednią frazą catch — wewnątrz tego destruktora, nie dopuszczając do jego „ucieczki”.

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