Что будет если бросить исключение из деструктора
Бросать исключения из деструктора в C++ крайне опасно и считается антипаттерном, особенно если это происходит во время размотки стека после другого исключения. Это может привести к завершению программы через std::terminate().
📌 Почему это проблема
Когда в C++ выбрасывается исключение, начинается размотка стека — поочерёдный вызов деструкторов локальных объектов. Если в процессе размотки стека ещё один деструктор выбросит исключение, то возникает двойное исключение, и это ведёт к аварийному завершению программы.
#include <iostream>
struct A {
~A() {
throw std::runtime_error("Exception from destructor");
}
};
int main() {
try {
A a;
throw std::runtime_error("Exception in try");
} catch (...) {
std::cout << "Caught exception\\n";
}
return 0;
}
💥 В этом примере программа не выведет "Caught exception", а вместо этого завершится с вызовом std::terminate().
📉 Что такое двойное исключение
-
Первое исключение выбрасывается в блоке try.
-
Начинается размотка стека: вызываются деструкторы всех локальных объектов.
-
Один из этих деструкторов (например, ~A()) выбрасывает второе исключение.
-
В этот момент уже обрабатывается одно исключение, и стандарт C++ запрещает одновременно обрабатывать два.
→ C++ не может выбрать, какое исключение важнее, поэтому вызывает:
std::terminate(); // завершение без возможности перехвата
📌 Правила из стандарта C++
Стандарт C++ требует, чтобы деструкторы не выбрасывали исключения, особенно в ситуациях, где они могут быть вызваны в процессе обработки другого исключения:
_If a destructor exits via an exception during stack unwinding due to another exception, the C++ runtime calls std::terminate()._
🛡 Как защититься
✅ Оборачивать потенциально опасный код в деструкторе в try-catch
~A() {
try {
// Код, который может выбросить исключение
} catch (...) {
// Перехватить и не выбрасывать дальше
std::cerr << "Exception suppressed in destructor\\n";
}
}
Такой подход гарантирует, что исключение не "вылетит наружу" из деструктора, и не вызовет std::terminate().
📘 Пример безопасного кода
struct SafeDestructor {
~SafeDestructor() {
try {
mayThrow();
} catch (const std::exception& e) {
std::cerr << "Suppressed: " << e.what() << '\\n';
}
}
void mayThrow() {
throw std::runtime_error("Oops");
}
};
🧠 Особенность с RAII
C++ активно использует RAII (Resource Acquisition Is Initialization): освобождение ресурсов привязано к деструкторам.
Например, std::lock_guard, std::unique_ptr, std::ofstream и т.д.
Если вы начнёте бросать исключения из деструкторов объектов, отвечающих за критические ресурсы — поведение программы станет непредсказуемым, особенно в условиях обработки ошибок.
💡 Почему вообще возникает желание бросить исключение из деструктора?
Иногда программисты хотят сообщить о том, что в деструкторе что-то пошло не так — например, не удалось закрыть файл, удалить временный файл, сохранить изменения. Но C++ не даёт встроенного безопасного механизма для этого.
Вместо исключений из деструктора используют:
-
Логирование (std::cerr, log.txt)
-
Хранение состояния ошибки во внешней переменной
-
Явный close() или commit() до уничтожения объекта
⚠️ Поведение при использовании noexcept
Начиная с C++11, все деструкторы по умолчанию объявляются как noexcept(true), что означает, что компилятор ожидает, что они не бросят исключение.
Если деструктор выбросит исключение, это будет нарушением noexcept, что также вызовет std::terminate():
struct MyStruct {
~MyStruct() noexcept {
throw std::runtime_error("oops"); // UB — нарушен noexcept
}
};
Компилятор может даже оптимизировать вызов, предполагая, что он не бросает — и это приведёт к неопределённому поведению (UB).
🔍 Итог по поведению
Сценарий | Поведение |
---|---|
Исключение внутри деструктора без try/catch | std::terminate() при размотке |
--- | --- |
Исключение, но обёрнуто в try/catch | Безопасно |
--- | --- |
Деструктор явно noexcept(false) | Исключение разрешено, но опасно |
--- | --- |
Деструктор по умолчанию noexcept(true) | Любое исключение вызывает terminate |
--- | --- |
📚 Заключение
Бросать исключения из деструктора — это опасно и нарушает стандартную практику C++. Даже если вам кажется, что это хороший способ сообщить об ошибке, безопаснее использовать:
-
Логирование
-
Объекты состояния
-
Специальные методы commit(), rollback() до разрушения объекта
-
Явный контроль ошибок вне деструктора
Всегда оборачивайте потенциально опасный код в try-catch внутри деструктора, чтобы не допустить аварийного завершения программы.