[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