Ответ
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-блока, оно обрабатывается обычным образом, так как раскрутка стека для первого исключения уже завершена.
Правильные практики для предотвращения проблем:
- Делать деструкторы и функции очистки
noexcept:~MyResourceHolder() noexcept { /* гарантированно не бросаем исключения */ } - Использовать
std::optionalили флаги состояния в функциях, которые могут вызываться из catch-блоков. - Логировать ошибки вместо выбрасывания исключений в критичных местах (деструкторы, обработчики исключений).
Пример безопасного подхода с 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 — всё, будет вам хиросима.
Так как же не облажаться? Правила простые, но жизненно важные, чувак.
-
Делай деструкторы и всякие cleanup-функции
noexcept. Это святое. Там не должно быть никакихthrow. Вообще. Никаких. Чистая, предсказуемая работа.~MyResourceHolder() noexcept { /* гарантированно не бросаем исключения */ } -
В catch-блоках — максимальная осторожность. Функции, которые ты оттуда вызываешь, должны быть безопасными. Используй
std::optional, возвращай коды ошибок, логируй проблемы, но не бросай новые исключения. Доверия к коду в таких местах — ебать ноль, поэтому будь параноиком. -
Если очень надо передать контекст ошибки, есть
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 посреди ночи на продекшене.