programowanieC++ Backend Developer

Jak działa system wyjątków (exceptions) w C++, i czym różni się od innych języków? Jak poprawnie projektować obsługę wyjątków w aplikacjach przemysłowych?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź

System wyjątków w C++ opiera się na mechanizmie try-catch oraz słowie kluczowym throw. W przypadku wystąpienia sytuacji wyjątkowej (np. błąd zasobu, naruszenie invariantów), wewnątrz bloku try inicjowane jest (wyrzucane) wyjątek za pomocą operatora throw. Operacje wyszukiwania odpowiedniego obsługującego (catch) odbywają się poprzez przeszukiwanie stosu do pierwszego pasującego typu.

Wažne szczegóły:

  • W przeciwieństwie do Java czy C#, wyjątki w C++ mogą być dowolnego typu (zwykle klasa pochodna od std::exception).
  • W C++ nie ma obowiązkowej specyfikacji wyjątków (jak checked exceptions w Java), zamiast tego w C++11 i później wprowadzono specyfikator noexcept.
  • Przy wyrzuceniu wyjątku następuje wywołanie destruktorów dla wszystkich obiektów na stosie pomiędzy punktem wyrzucenia a punktem obsługi — ważne jest, aby przestrzegać RAII.

Projektowanie:

  • Rzucaj tylko wyjątki „nieoczekiwanych” błędów, do innych używaj kodu zwrotu.
  • Chwytaj po stałej referencji — catch(const std::exception& e).
  • GNU zaleca wyrzucanie obiektów, które są kopiowane, a nie wskaźników (unikaj pamięci na stercie).

Przykład kodu:

#include <iostream> #include <stdexcept> void mayFail(bool fail) { if (fail) throw std::runtime_error("Błąd procesu"); } int main() { try { mayFail(true); } catch (const std::exception& ex) { std::cout << "Złapano wyjątek: " << ex.what() << std::endl; } }

Pytanie z pułapką

Co się stanie, jeśli wyjątek zostanie wyrzucony z destruktora podczas przekazywania innego wyjątku w górę stosu?

Odpowiedź: Program zakończy się awaryjnie przez wywołanie std::terminate(), ponieważ podczas obsługi wyjątku nie można generować nowego wyjątku (double exception), w przeciwnym razie narusza to odwijanie stosu.

Przykład:

struct CrashOnDestruct { ~CrashOnDestruct() noexcept(false) { throw std::runtime_error("Błąd w destruktorze!"); } }; void func() { CrashOnDestruct obj; throw std::logic_error("Obsługa błędu"); } int main() { try { func(); } catch (...) { } } // Zakończy się przez std::terminate()

Przykłady rzeczywistych błędów z powodu nieznajomości szczegółów tematu


Historia

W dużej aplikacji finansowej wyjątki były rzucane z destruktora klasy-opakowania nad bazą danych. Jedna z kluczowych transakcji w przypadku wystąpienia błędu inicjowała wyjątek, a przy wyjściu przez unwind był wywoływany destruktor, który również rzucał błąd. Cała aplikacja kończyła się awarią, tracąc pracę użytkowników. Programiści musieli pilnie zastąpić throw w destruktorach zapisywaniem logów i poprawną obsługą.


Historia

Mikrousługa obsługiwała wyjątki typu: catch(std::exception). Jeden z wątków wyrzucał wyjątki typów użytkownika (nie dziedziczyły od std::exception). Takie błędy nie były chwytane, co prowadziło do niespodziewanego zerwania połączenia i wycieku pamięci.


Historia

Brak specyfikatora noexcept w metodach przenoszenia kontenerów spowodował, że standardowa biblioteka używała wolnej kopii zamiast szybkiej operacji move; znaczny spadek wydajności został wykryty tylko podczas testów obciążeniowych.