Что будет если бросить исключение из деструктора


Бросать исключения из деструктора в 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 внутри деструктора, чтобы не допустить аварийного завершения программы.