← Назад к вопросам

Что знаешь про вызов исключения в деструкторе?

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");
    }
}

Резюме

Главное правило: НИКОГДА не выбрасывай исключения из деструктора.

Почему:

  1. Приводит к std::terminate() если произойдёт во время stack unwinding
  2. Делает обработку ошибок непредсказуемой
  3. Нарушает ожидания от RAII

Что делать:

  1. Объявляй деструкторы как noexcept
  2. Логируй ошибки вместо исключений
  3. Предоставь явный метод close() для обработки ошибок
  4. Ловишь исключения в деструкторе и обрабатываешь их безопасно
Что знаешь про вызов исключения в деструкторе? | PrepBro