Во всём разделе Exception handling упоминание "undefined behavior" встречается только однажды: при использовании нестатических членов класса в обработчике function-try-block конструтора:
Referring to any non-static member or base class of an object in the handler for a function-try-block of a constructor or destructor for that object results in undefined behavior.
Т.о. даже если не вдаваться в подробности, об UB в вашем случае речи нет.
Прямых слов о том, что в процессе раскрутки стека в деструкторе может быть вызвано и обработано новое исключение, я в стандарте не нашёл. Однако, есть заметка:
If a destructor directly invoked by stack unwinding exits via an exception, std::terminate is invoked.
Мой перевод:
Если деструктор, вызванный напрямую раскруткой стека, завершится исключением, то будет вызвано std::terminate.
Логично предположить, что если деструктор не завершается исключением (в том числе, если вызвать и тут же обработать его), то к вызову std::terminate это не приведёт. С неопределённым поведением сложнее, т.к. формально, если явно не описано поведение, то может ожидаться неявное неопределённое. На данный случай это всё же вряд ли распространяется, т.к. подход с перехватом исключения в деструкторе описан в том числе и в C++ Super-FAQ на isocpp.org:
You can throw an exception in a destructor, but that exception must not leave the destructor;
Почему вообще исключениям в деструкторе уделяется такое внимание? Деструктор автоматически неявно вызывается в двух случаях:
- при нормальном завершении жизни объекта (при выходе из блока, если объект создан на стеке; или при вызове
delete, если создан через new);
- при аварийном выходе из блока по исключению (т.н. раскрутка стека).
В последнем случае добавление нового исключения в деструкторе (без его обработки там же) приведёт к тому, что оно как бы будет улетать к обработчику первичного исключения. Однако, что в этом случае там делать с присутствующими одновременно двумя исключениями совершенно непонятно, поэтому в стандарте принято решение просто сдаться и завершить программу через std::terminate.
Если же исключение, возникшее в деструкторе, в нём же и обработать, то даже при раскрутке стека (наличии первичного исключения) ситуации, что один обработчик (т.е. catch блок) столкнётся с двумя активными исключениями, не возникнет. Поведение программы при этом может быть реализовано вполне понятным образом.
Вообще, выкидывать исключения из деструктора наружу можно. Как минимум надо добавить noexcept(false), т.к. по умолчанию деструкторы неявно считаются невыкидивающими исключений. Но главное - это убедиться, что мы не находимся в этот момент в режиме раскрутки стека. Примерно так:
#include <iostream>
#include <exception>
struct X {
X() : except_count(std::uncaught_exceptions()) {}
~X() noexcept(false) {
if (except_count == std::uncaught_exceptions()) {
std::cout << "X can throw in destructor\n";
throw 1;
}
std::cout << "X stack unwinding, can't throw out\n";
}
int except_count;
};
int main() {
try {
X x;
throw 2; // в зависимости от наличия этой строки будем попадать в разные ветки
}
catch(...) {
std::cout << "caught in main\n";
}
}
Самое сложное здесь, это обеспечить, что исключение может покинуть деструктор только в соответствующем блоке с проверкой, а также что в этом блоке нет других вложенных объектов, выкидывающих исключение из своего деструктора, не опираясь на схожую проверку наличия активной раскрутки стека. Иначе всё это снова приведёт к необработанным исключениям и std::terminate в итоге. Так что намного проще не допускать выхода исключений из деструктора.
В целом, сложности использования исключений в деструкторах в C++ возникают из-за того, что при генерации исключения сначала происходит раскрутка стека, и только потом мы попадаем в обработчик. Если бы мы попадали в обработчик сразу, и откладывали раскрутку стека на потом, двойных активных исключений можно было бы избежать.
когда разрушение объекта во время раскрутки стека *завершается* выдачей исключения,. Т.е. бросать исключения не запрещено. Запрещено их выбрасывать из тела деструктора. – Chorkov Dec 09 '21 at 13:36