Что такое double exception (двойное исключение) в 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(); // Безопасная очистка
    }
}

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

Ответ 18+ 🔞

Ёпта, слушай, вот история про исключения, которая может такую дичь устроить, что просто волосы дыбом встанут. Представь, твой код уже наебнулся, летит первое исключение, всё рушится, стек раскручивается... И тут, на тебе, какая-то функция очистки cleanup() подкидывает вторую гранату! Это и есть double exception, ёпта.

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

#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;
}

Всё, приехали. cleanup() подвешивает — и программа накрывается медным тазом. Удивление пиздец, да?

А как сейчас-то, с C++11? Ну, стандарт поумнел, конечно. Теперь std::terminate() зовут только если второе исключение вылетает прямо во время раскрутки стека для первого. То есть, если ты уже в catch-блоке — раскрутка закончилась, и второе исключение будет обрабатываться как обычное. Но это не значит, что можно расслабиться. Если это случится до того, как управление дошло до catch — всё, будет вам хиросима.

Так как же не облажаться? Правила простые, но жизненно важные, чувак.

  1. Делай деструкторы и всякие cleanup-функции noexcept. Это святое. Там не должно быть никаких throw. Вообще. Никаких. Чистая, предсказуемая работа.

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

  3. Если очень надо передать контекст ошибки, есть std::nested_exception. Это как вложить одну проблему в другую, более красивую упаковку. Безопасно и информативно.

#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(); // Безопасная очистка
    }
}

Главный вывод, который ты должен запомнить, как «Отче наш»: исключения в деструкторах и во время обработки других исключений — это прямой билет в ад. Избегай этого, как чёрт ладана. Пиши безопасный код, и будет тебе счастье, а не неожиданный terminate посреди ночи на продекшене.