W C++ obsługa wyjątków wiąże się z mechanizmem stack unwinding — automatycznym destruktorowaniem lokalnych obiektów na stosie wywołań, które zostały utworzone przed wystąpieniem wyjątku. To zjawisko jest ważne dla prawidłowego zwalniania zasobów.
Początkowo, podczas projektowania C++, brakowało wbudowanego garbage collectora, więc zadanie zwalniania zasobów (pamięci, plików, gniazd) spadło na programistę. Obsługa wyjątków miała zapewnić poprawne zwalnianie w przypadku wycofywania kodu z powodu błędu.
Bez mechanizmu automatycznej destrukcji obiektów w przypadku wyjątków występuje wyciek zasobów. Jeśli nie zwalniać zasobów ręcznie w każdym bloku catch, kod staje się skomplikowany i niepewny.
C++ implementuje stack unwinding, gdy przy rzucaniu wyjątku wszystkie obiekty umieszczone na stosie w zakresie widoczności przed throw są niszczone w odwrotnej kolejności do ich utworzenia. Ich destruktory są wywoływane automatycznie.
Klasycznym sposobem na ciągłe zwalnianie zasobów jest stosowanie wzorca RAII (Resource Acquisition Is Initialization): wszystkie zasoby są "owinięte" w obiekty, które przy destrukcji zwalniają posiadany zasób.
Przykład kodu:
#include <iostream> #include <stdexcept> struct FileHandle { FILE* file; FileHandle(const char* path) { file = fopen(path, "r"); if (!file) throw std::runtime_error("Nie można otworzyć pliku"); } ~FileHandle() { if (file) fclose(file); } }; void processFile(const char* path) { FileHandle fh(path); // Wycofuje, jeśli wystąpi throw // ... operacje na pliku throw std::runtime_error("Jakiś błąd"); // fclose wywoła się automatycznie }
Kluczowe cechy:
Dlaczego nie można po prostu użyć try-catch i ręcznie wywołać delete lub fclose w bloku catch, aby uniknąć wycieków?
Odpowiedź:
Jest to niewygodne i niepewne: łatwo zapomnieć zamknąć zasób, szczególnie przy wielu punktach wyjścia lub zagnieżdżonych zasobach. Destruktory są wywoływane nawet, gdy wyjątek "przelatuje" przez funkcję, catch do tego nie jest potrzebny.
Czy obiekty w stercie (heap) zostaną zniszczone, jeśli nie są "owinięte" w obiekty na stosie podczas stack unwinding?
Odpowiedź:
Nie. Stack unwinding wywołuje destruktory tylko dla obiektów umieszczonych na stosie. Aby obiekty heap były poprawnie niszczone, muszą być zarządzane przez obiekt na stosie (na przykład przez wskaźniki inteligentne).
Co się stanie, jeśli destruktor podczas stack unwinding sam rzuca wyjątek?
Odpowiedź:
Jeśli w trakcie stack unwinding podczas niszczenia jakiegoś obiektu zostanie rzucony drugi wyjątek, program zakończy się wywołaniem std::terminate(). Nigdy nie rzucaj wyjątków z destruktorów!
Programista ręcznie zamyka pliki i zwalnia pamięć przez bloki catch, nie stosując RAII.
Zalety:
Wady:
Pliki i zasoby są owijane w klasy-owoły RAII. W rezultacie: zwalnianie odbywa się w destruktorach, niezależnie od throw/catch.
Zalety:
Wady: