[PL] Pamięć, konstruktor kopiujący i obsługa wyjątków

Napewno Ameryki nie odkryłem, aczkolwiek przy mojej dalszej zabawie z Native VC++ w VS2008 przy prostych testach natknąłem się na coś na co wcześniej chyba nie zwróciłem uwagi (a przynajmniej świadomie).

Po pierwsze, testowałem sobie jak się kompilator i aplikacja zachowa, gdy zastosuję różne podejścia do zrzucania wyjątków. Pierwszy to catch (Exception & err) , drugi to catch(Exception * err) oraz ostatni catch (Exception err) z odpowiednim throw'ami oczywiście. Zgodnie z wieloma publikacjami najlepszych praktyk i czy chociażby Thinking in C++ Bruce'a Eckela zalecana jest oczywiście obsługa wyjątków przez referencję.
Jeden z podstawowych powodów jaki jest opisany w jego książce to chociażby fakt, że przez brak referencji zadziała konstruktor kopiujący zadeklarowanego w catch obiektu a nie oryginalnie zrzuconego. Przykład z jego książki ilustrujący taką sytuację jest poniżej:

//:C01:Catchref.cpp
// Why catch by reference?
//{L} ../TestSuite/Test

#include <iostream>
using namespace std;

class Base {
public:
  virtual void what() {
    cout << "Base" << endl;
  }
};

class Derived : public Base {
public:
  void what() {
    cout << "Derived" << endl;
  }
};

void f() { throw Derived(); }

int main() {
  try {
    f();
  } catch(Base b) {
    b.what();
  }
  try {
    f();
  } catch(Base& b) {
    b.what();
  }
} ///:~

Oraz wynik uruchomienia takiego programu:

Base
Derived

Przypadek Bruce’a w ogóle nie rozpatruje wykorzystania wskaźnika. Więc jeśli wykorzystujecie wskaźnik przy throw-catch to drugi powód, jaki mogę dodać od siebie to fakt wygenerowania memory leak’a.
W przypadku referencji (a nawet w przypadku jej braku) o cykl życia obiektu kompilator już sobie zadba.
W przypadku wskaźnika wykonanie kodu:

            try {
//…
                    throw new SimpleException();
             }
             catch (SimpleException * err) {
                    //error handling
                    delete err; //put correct object disposal code
             }

Bez linijki zawierającej delete (lub bardziej skomplikowaną formułę dla bardziej wyrafinowanych obiektów) powstaje memory leak.
Wydaje się banalne, ale poprzez przykład wielu przykładów kodu w C++, który można znaleźć w sieci (czy chociażby przykładów kodu w Managed C++), można nabrać niezdrowych nawyków (ref [1] [2]). Native C++ nie wybaczy takiego błedu.

To jednak nie jest to co mnie zaintrygowało. Skupiłem się na działaniu konstruktora kopiującego w przypadku zrzucanych wyjątków przez referencję i bez referencji, ale nie przez wskaźnik.
Wydawało mi się, że gdy przekażę przez referencję to konstruktor kopiujący nie powinien zadziałać, lecz powinien w tle powinien zostać przekazany wskaźnik do obiektu i obsłużony w catch zgodnie z swoim cyklem życia.
Prosty debug wykazał, że konstruktor kopiujący jest wywoływany, a próba zablokowania wywołania konstruktora kopiujacego (przykład poniżej) powoduje błąd kompilacji.

class SimpleException {
public:
       SimpleException() { }
       SimpleException(const SimpleException & );

       int i;
};

i wynik:

# error LNK2001: unresolved external symbol "public: __thiscall SimpleException::SimpleException(class SimpleException const &)" (??0SimpleException@@QAE@ABV0@@Z) TheFramework.Tests.Runtime.obj TheFramework.Tests.Runtime
# fatal error LNK1120: 1 unresolved externals C:\Users\danieb\Documents\Visual Studio 2008\Projects\TheFramework\Debug\TheFramework.Tests.Runtime.exe TheFramework.Tests.Runtime

To też w sumie jest zgodne z założeniami opisanymi chociażby w książkach Bruce’a Eckela.
Zasadnicza różnica w kontekście pamięci pomiędzy przekazaniem wyjatku przez referencję i bez jest w ilości strzorzonych kopii: catch (Exception err) niewiadomo dlaczego robi dwa wywołania konstruktora kopiującego. Rozumiem jednak, że to jest konsekwencja implementacji w Visual Studio czegoś co znamy jako "automatically generated temporaries".

Ale to już inna bajka. Wracając do braku konstruktora kopiującągo (jak w przypadku powyższej klasy) pojawia się kolejne pytanie: dlaczego ten sam mechanizm nie zadziałał, gdy całkiem jawnie nakazałem wykonanie konstruktora kopiującego, którego brak w przypadku kodu:

int _tmain(int argc, _TCHAR* argv[])
{
       SimpleException e1;
       SimpleException e2;

e1.i = 12;
e2 = e1;

}

I zamiast błędu przy kompilacji (czego się dla odmiany spodziewałem), dostałem działający EXE, który sobie poradził z przepisaniem wartości właściwości i.

Frapujące, wręcz akademicko frapujące. Znowu prewencyjnie ubezpieczam się, że odwykłem od pisania w języku, w którym zamiast po prostu dochodzić do celu generuję sobie problemy, które po drodzę muszę rozwiązać. Mam urlop, mogę sobie na to pozwolić J

Technorati Tagi: Polish Posts,coding,C++,geeks