← Назад к вопросам
Что знаешь про вызов исключения в деструкторе?
2.0 Middle🔥 141 комментариев
#Исключения и обработка ошибок#Язык C++
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Что знаешь про вызов исключения в деструкторе?
Вызов исключений в деструкторе — это критическая ошибка в C++, которая почти всегда должна избегаться. Это один из самых опасных паттернов в языке, способный привести к необопытным последствиям.
Почему это опасно?
Проблема 1: Двойное исключение (Exception During Stack Unwinding)
Когда происходит обработка исключения, программа разворачивает стек вызовов (stack unwinding). Если в этот момент из деструктора выбрасывается новое исключение, это приводит к terminate().
void dangerous_example() {
class MyObject {
public:
~MyObject() {
throw std::runtime_error("Error in destructor");
}
};
try {
MyObject obj;
throw std::logic_error("First error"); // Исключение 1
} catch (const std::logic_error& e) {
// Здесь происходит разворачивание стека (stack unwinding)
// Вызывается ~MyObject()
// ~MyObject() выбрасывает исключение (исключение 2)
// Программа вызывает std::terminate()
}
}
// Результат: std::terminate() вызывается, программа падает аварийно
Визуализация процесса
Нормальное выполнение:
try block → исключение → catch block → destructor
Проблема:
try block → исключение 1 → unwinding → destructor → исключение 2 → CRASH!
^Неопасно!
Правило №1: Никогда не выбрасывай исключения из деструктора
Плохо: исключение в деструкторе
class File {
private:
std::FILE* handle;
public:
~File() {
if (!handle) {
throw std::runtime_error("File handle is null"); // ОПАСНО!
}
fclose(handle);
}
};
int main() {
try {
File f;
throw std::logic_error("Something went wrong");
} catch (const std::logic_error& e) {
// Когда выходим из блока, вызывается ~File()
// ~File() выбрасывает исключение
// std::terminate() вызывается ➜ CRASH!
}
}
Хорошо: логирование вместо исключений
class File {
private:
std::FILE* handle;
public:
~File() noexcept { // Пообещаем, что не выбросим исключение
if (!handle) {
std::cerr << "Warning: File handle is null" << std::endl;
return; // Логируем, но не выбрасываем
}
if (fclose(handle) != 0) {
std::cerr << "Error: Failed to close file" << std::endl;
// Всё равно не выбрасываем
}
}
};
Правило №2: Используй noexcept
Старайся явно объявлять деструкторы как noexcept:
class MyClass {
public:
~MyClass() noexcept { // Обещаем не выбросить исключение
// Безопасный код, без исключений
}
};
// Это хорошая практика — компилятор проверит
// Если в теле будет throw, будет ошибка компиляции
Примечание: В C++11 деструкторы implicitly noexcept, но лучше быть явным.
Что делать, если нужна обработка ошибок в деструкторе?
Вариант 1: Явное закрытие с обработкой
class Database {
private:
Connection* conn;
bool is_open;
public:
Database() : conn(nullptr), is_open(false) {}
void open(const std::string& url) {
conn = new Connection(url);
is_open = true;
}
// Явная функция для закрытия с обработкой ошибок
void close() { // Не деструктор!
if (!is_open) return;
try {
conn->disconnect();
} catch (const std::exception& e) {
std::cerr << "Error disconnecting: " << e.what() << std::endl;
// Можем обработать ошибку
}
is_open = false;
}
~Database() noexcept {
// Деструктор просто вызывает close(), не выбрасывая
try {
close();
} catch (...) {
// Ловим всё, но не выбрасываем
std::cerr << "Unexpected error in destructor" << std::endl;
}
}
};
int main() {
Database db;
db.open("postgresql://localhost");
// Обработка с исключениями
try {
db.close(); // Явное закрытие
} catch (const std::exception& e) {
std::cerr << "Failed to close: " << e.what() << std::endl;
}
// Деструктор вызывается без проблем
}
Вариант 2: RAII с двухфазным destructor
class File {
private:
std::FILE* handle;
bool closed;
public:
File(const std::string& name) : handle(nullptr), closed(false) {
handle = std::fopen(name.c_str(), "r");
if (!handle) {
throw std::runtime_error("Cannot open file");
}
}
// Явная функция для закрытия
void close() {
if (closed) return;
int result = std::fclose(handle);
if (result != 0) {
throw std::runtime_error("Failed to close file");
}
closed = true;
}
// Деструктор просто проверяет, что файл закрыт
~File() noexcept {
if (!closed && handle) {
std::fclose(handle); // Тихо закрываем, без проверок
}
}
};
int main() {
try {
File f("data.txt");
// ...
f.close(); // Явное закрытие с обработкой ошибок
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
// Деструктор вызывается безопасно
}
Вариант 3: Логирование вместо исключений
class NetworkSocket {
private:
int socket_fd;
std::function<void(const std::string&)> error_logger;
public:
NetworkSocket(std::function<void(const std::string&)> logger)
: socket_fd(-1), error_logger(logger) {}
~NetworkSocket() noexcept {
if (socket_fd != -1) {
if (::close(socket_fd) != 0) {
// Логируем ошибку, но не выбрасываем
if (error_logger) {
error_logger("Failed to close socket");
}
}
}
}
};
Специальные случаи
С std::unique_ptr и пользовательскими deleters
// Если custom deleter выбросит исключение, программа упадёт
std::unique_ptr<Resource> res(
new Resource(),
[](Resource* r) { // Custom deleter
// Если здесь будет throw, это очень опасно
if (!cleanup(r)) {
// throw std::runtime_error("Cleanup failed"); // НЕ ДЕЛАЙ ЭТОГО!
std::cerr << "Cleanup failed" << std::endl; // Лучше логируй
}
delete r;
}
);
Проверка: std::terminate
// Это поведение можно проверить
#include <exception>
#include <iostream>
void my_terminate_handler() {
std::cout << "Program is terminating!" << std::endl;
std::abort();
}
int main() {
std::set_terminate(my_terminate_handler);
try {
throw std::runtime_error("Error");
} catch (...) {
// Деструктор выбросит исключение ➜ my_terminate_handler() вызовется
}
}
Best Practices
// 1. Всегда объявляй деструктор как noexcept
~MyClass() noexcept { }
// 2. Никогда не выбрасывай исключения из деструктора
~MyClass() noexcept {
// throw std::exception(); // НИКОГДА!
}
// 3. Используй try-catch для блокировки исключений
~MyClass() noexcept {
try {
cleanup();
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
// Не выбрасываем
}
}
// 4. Предоставь явный метод для очистки
class Resource {
public:
void close() {
// Может выбросить исключение
}
~Resource() noexcept {
// Деструктор безопасен
}
};
// 5. Логируй ошибки, но не выбрасывай
~MyClass() noexcept {
if (operation_failed()) {
log_error("Operation failed in destructor");
}
}
Резюме
Главное правило: НИКОГДА не выбрасывай исключения из деструктора.
Почему:
- Приводит к std::terminate() если произойдёт во время stack unwinding
- Делает обработку ошибок непредсказуемой
- Нарушает ожидания от RAII
Что делать:
- Объявляй деструкторы как noexcept
- Логируй ошибки вместо исключений
- Предоставь явный метод close() для обработки ошибок
- Ловишь исключения в деструкторе и обрабатываешь их безопасно