Когда использование throw в деструкторе в C++ приводит к неопределённому поведению?

«Когда использование throw в деструкторе в C++ приводит к неопределённому поведению?» — вопрос из категории C++ Core, который задают на 25% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В C++ деструктор по умолчанию неявно помечен как noexcept (начиная с C++11). Если деструктор, вызванный в процессе раскрутки стека (stack unwinding) из-за другого активного исключения, попытается выбросить своё исключение, программа немедленно завершится через вызов std::terminate().

Почему это опасно: Во время раскрутки стека уничтожаются объекты в области видимости для очистки ресурсов. Если на этом этапе возникнет новое исключение, система исключений C++ окажется в неконсистентном состоянии (какое из двух исключений обрабатывать?), что приводит к аварийному завершению.

Пример проблемного кода:

#include <iostream>
#include <stdexcept>

struct ResourceHolder {
    ~ResourceHolder() noexcept(false) { // Явно разрешаем исключения – опасно!
        // ... какая-то очистка, которая может завершиться ошибкой
        throw std::runtime_error("Cleanup failed in destructor!");
    }
};

int main() {
    try {
        ResourceHolder rh;
        throw std::logic_error("Something went wrong in main logic"); // Первое исключение
        // Деструктор `rh` будет вызван при раскрутке стека.
        // Если он выбросит своё исключение -> вызовется std::terminate()
    } catch (const std::exception& e) {
        // Сюда мы, скорее всего, не попадём
        std::cout << "Caught: " << e.what() << std::endl;
    }
}
// Программа завершится через std::terminate()

Правильный подход:

  1. Не бросать исключения в деструкторах. Это главное правило.
  2. Логировать ошибки. Если операция очистки может закончиться неудачей (например, закрытие файла или сетевого соединения), лучше записать ошибку в лог, но не прерывать выполнение.
  3. Предоставлять отдельный метод close() или release(). Для ресурсов, требующих явного освобождения с проверкой ошибок, вынесите эту логику в отдельный метод, который можно вызвать до разрушения объекта.

    struct SafeFile {
    FILE* handle = nullptr;
    
    bool close() { // Явный метод для обработки ошибок
        if (handle && fclose(handle) != 0) {
            // Логируем ошибку, но не бросаем исключение
            perror("fclose failed");
            return false;
        }
        handle = nullptr;
        return true;
    }
    
    ~SafeFile() noexcept { // Деструктор теперь безопасен
        if (handle) {
            // Игнорируем ошибку, так как в деструкторе мы бессильны
            fclose(handle);
        }
    }
    };