Что такое double exception (двойное исключение) в C++?

«Что такое double exception (двойное исключение) в C++?» — вопрос из категории C++ Core, который задают на 30% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Double exception (двойное исключение) — это ситуация, когда второе исключение выбрасывается во время обработки (раскрутки стека) первого исключения. До стандарта C++11 это приводило к немедленному вызову std::terminate() и аварийному завершению программы.

Проблемный пример (до C++11):

#include <iostream>
#include <stdexcept>

void cleanup() {
    throw std::logic_error("Ошибка в cleanup!"); // Второе исключение во время раскрутки стека!
}

void foo() {
    throw std::runtime_error("Первичная ошибка в foo");
}

int main() {
    try {
        foo();
    } catch (const std::runtime_error& e) {
        std::cout << "Поймано: " << e.what() << 'n';
        cleanup(); // Выбрасывает второе исключение -> std::terminate() до C++11
    }
    return 0;
}

Как обрабатывается сейчас (C++11 и новее): Стандарт C++11 ввёл понятие исключения в процессе обработки исключения (exception during stack unwinding). Если такое происходит, вызывается std::terminate(). Однако, если второе исключение генерируется непосредственно внутри catch-блока, оно обрабатывается обычным образом, так как раскрутка стека для первого исключения уже завершена.

Правильные практики для предотвращения проблем:

  1. Делать деструкторы и функции очистки noexcept:
    ~MyResourceHolder() noexcept { /* гарантированно не бросаем исключения */ }
  2. Использовать std::optional или флаги состояния в функциях, которые могут вызываться из catch-блоков.
  3. Логировать ошибки вместо выбрасывания исключений в критичных местах (деструкторы, обработчики исключений).

Пример безопасного подхода с std::nested_exception (C++11):

#include <iostream>
#include <stdexcept>
#include <exception>

void safe_cleanup() noexcept { /* ... */ }

void foo() {
    try {
        throw std::runtime_error("Ошибка уровня 1");
    } catch (...) {
        // Сохраняем текущее исключение и бросаем новое с контекстом
        std::throw_with_nested(std::logic_error("Добавлен контекст"));
    }
}

int main() {
    try {
        foo();
    } catch (const std::exception& e) {
        std::cerr << "Ошибка: " << e.what() << 'n';
        try {
            std::rethrow_if_nested(e); // Раскручиваем вложенное исключение, если есть
        } catch (const std::exception& nested) {
            std::cerr << "tПричина: " << nested.what() << 'n';
        }
        safe_cleanup(); // Безопасная очистка
    }
}

Главное правило: исключения в деструкторах и во время обработки других исключений — это опасные ситуации, которых нужно избегать.