Когда могут не вызваться деструкторы при исключении?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Когда деструкторы не вызываются при исключении
Это важный вопрос о надёжности C++ приложений. Stack unwinding с вызовом деструкторов — это основа RAII и управления ресурсами в C++. Но есть ситуации, когда вызов гарантирован НЕ происходит.
1. Исключение в конструкторе
Проблема: Если конструктор выбросит исключение, деструктор вообще не вызовется, потому что объект не был полностью построен.
class Resource {
public:
int* buffer;
Resource(size_t size) {
buffer = new int[size];
if (size > 1000000) {
throw std::runtime_error("Size too large");
// Деструктор НИКОГДА не вызовется!
// buffer остаётся висящим указателем
}
}
~Resource() {
delete[] buffer;
}
};
int main() {
try {
Resource r(2000000); // Выбросит исключение
} catch (...) {
// Утечка памяти! Деструктор не вызван
}
}
Решение — двухэтапная инициализация:
class Resource {
int* buffer;
bool initialized;
public:
Resource() : buffer(nullptr), initialized(false) {}
void init(size_t size) { // Отдельный метод инициализации
buffer = new int[size];
if (size > 1000000) {
delete[] buffer;
throw std::runtime_error("Size too large");
}
initialized = true;
}
~Resource() {
if (initialized && buffer) {
delete[] buffer;
}
}
};
// Лучше: используй smart pointers
class ResourceModern {
std::unique_ptr<int[]> buffer;
public:
ResourceModern(size_t size) : buffer(std::make_unique<int[]>(size)) {
if (size > 1000000) {
throw std::runtime_error("Size too large");
// unique_ptr деструктор БУДЕТ вызван при unwinding
}
}
};
2. Исключение в деструкторе
Проблема: Если деструктор выбросит исключение во время stack unwinding'а, программа либо вызовет std::terminate(), либо потеряет исходное исключение.
class Database {
public:
~Database() {
close(); // Что если close() выбросит исключение?
}
void close() {
// Попытка закрыть соединение
if (connection_broken) {
throw std::runtime_error("Connection error");
}
}
};
int main() {
try {
Database db;
// Во время выхода из scope, деструктор вызывает close(),
// который выбрасывает исключение
// → std::terminate() вызывается, программа падает!
} catch (...) {}
}
Правило: деструкторы НИКОГДА не должны выбрасывать исключения!
class DatabaseSafe {
public:
~Database() noexcept { // noexcept гарантирует отсутствие исключений
try {
close();
} catch (const std::exception& e) {
// Логируем ошибку, но не выбрасываем
std::cerr << "Error closing database: " << e.what() << std::endl;
}
}
};
3. Исключение до построения объекта
Проблема: Если исключение выбросилось в инициализаторе списка параметров конструктора, предыдущие уже инициализированные члены могут не очиститься правильно.
class MyClass {
public:
std::unique_ptr<Resource> res1;
std::unique_ptr<Resource> res2;
std::string name;
MyClass(const std::string& n)
: res1(std::make_unique<Resource>(100)),
res2(std::make_unique<Resource>(throwIfLarge())), // Может выбросить
name(n) {
// res1 уже создан и будет успешно удалён
// res2 не создан, так что его деструктор не вызовется
// name уже инициализирован и удалится
// Но порядок удаления гарантирован в обратном порядке инициализации
}
};
С modern C++ и smart pointers это обычно безопасно, но важно помнить о порядке инициализации.
4. Вызов std::exit() или std::abort()
Проблема: Глобальные объекты и объекты на stack не удаляются.
class Logger {
public:
~Logger() {
std::cout << "Logger destroyed" << std::endl;
}
};
Logger global_logger;
int main() {
std::exit(0); // Деструктор global_logger МОЖЕТ не вызваться
// (зависит от реализации, но обычно вызовется)
}
Правило: std::exit() вызывает деструкторы глобальных объектов, но std::abort() — НЕ вызывает. Избегай std::abort() в коде.
5. Исключение в глобальных конструкторах
Проблема: Если конструктор глобального объекта выбросит исключение, программа прекращает работу ещё до вызова main().
struct GlobalResource {
GlobalResource() {
throw std::runtime_error("Can't initialize global resource");
// Нет обработки этого исключения!
// Программа просто умирает
}
};
GlobalResource gr; // Исключение выбросится при загрузке библиотеки
int main() {
// Никогда не будет достигнут
}
Решение: Используй lazy initialization для глобальных ресурсов.
6. Исключение в signal handler'е
Проблема: Исключения НЕ могут быть выброшены из signal handler'ов!
void signal_handler(int sig) {
throw std::runtime_error("Signal received!"); // UNDEFINED BEHAVIOR!
}
int main() {
std::signal(SIGINT, signal_handler);
}
7. Исключение при выполнении с optimization уровнем -fno-exceptions
Проблема: Если компилируешь с -fno-exceptions, обработка исключений отключена.
g++ -fno-exceptions program.cpp # Исключения не работают!
Практические рекомендации
// ✅ ПРАВИЛЬНО - используй RAII
class ConnectionPool {
std::vector<std::unique_ptr<Connection>> connections;
std::mutex m;
public:
~ConnectionPool() noexcept { // noexcept!
// unique_ptr автоматически удалит connections
}
};
// ✅ ПРАВИЛЬНО - обработай исключения в деструкторе
~MyClass() noexcept {
try {
cleanup();
} catch (const std::exception& e) {
LOG_ERROR("Cleanup failed: ", e.what());
}
}
// ❌ НЕПРАВИЛЬНО - выброс из деструктора
~MyClass() { // БЕЗ noexcept!
throw std::runtime_error("Error!"); // ОПАСНО!
}
Чек-лист для production кода
- Все деструкторы помечены
noexcept - Деструкторы не выбрасывают исключения
- Используются smart pointers (unique_ptr, shared_ptr)
- Критичные ресурсы имеют fail-safe cleanup
- Глобальные объекты инициализируются безопасно
- Исключения в конструкторах обработаны