- PVSM.RU - https://www.pvsm.ru -
Многие знатоки C++ (например, Герб Саттер [1]) учат нас, что бросать исключения в деструкторах плохо, потому что в деструктор можно попасть во время раскрутки стека при уже выброшенном исключении, и если в этот момент будет выброшено ещё одно исключение, в результате будет вызван std::terminate(). Стандарт языка C++17 (здесь и далее я ссылаюсь на свободно доступную версию драфта N4713 [2]) на эту тему сообщает нам следующее:
18.5.1 The std::terminate() function [except.terminate]
1 In some situations exception handling must be abandoned for less subtle error handling techniques. [ Note:
These situations are:
…
(1.4) when the destruction of an object during stack unwinding (18.2) terminates by throwing an exception, or
…
— end note ]
Проверим на простом примере:
#include <iostream>
class PrintInDestructor {
public:
~PrintInDestructor() noexcept {
std::cerr << "~PrintInDestructor() invokedn";
}
};
void throw_int_func() {
std::cerr << "throw_int_func() invokedn";
throw 1;
}
class ThrowInDestructor {
public:
~ThrowInDestructor() noexcept(false) {
std::cerr << "~ThrowInDestructor() invokedn";
throw_int_func();
}
private:
PrintInDestructor member_;
};
int main(int, char**) {
try {
ThrowInDestructor bad;
throw "BANG!";
} catch (int i) {
std::cerr << "Catched int exception: " << i << "n";
} catch (const char* c) {
std::cerr << "Catched const char* exception: " << c << "n";
} catch (...) {
std::cerr << "Catched unknown exceptionn";
}
return 0;
}
Результат:
~ThrowInDestructor() invoked
throw_int_func() invoked
~PrintInDestructor() invoked
terminate called after throwing an instance of 'int'
Aborted
Обратите внимание, что деструктор PrintInDestructor всё же вызывается, т.е. после выбрасывания второго исключения раскрутка стека не прерывается. В Стандарте (тот же самый пункт 18.5.1) на эту тему сказано следующее:
2… In the situation where no matching handler is found,
it is implementation-defined whether or not the stack is unwound before std::terminate() is called. In
the situation where the search for a handler (18.3) encounters the outermost block of a function with a
non-throwing exception specification (18.4), it is implementation-defined whether the stack is unwound,
unwound partially, or not unwound at all before std::terminate() is called ...
Я проверял этот пример на нескольких версиях GCC (8.2, 7.3) и Clang (6.0, 5.0), везде раскрутка стека продолжается. Если вы встретите компилятор, где implementation-defined по-другому, пожалуйста, напишите об этом в комментариях.
Следует заметить также, что std::terminate() при раскрутке стека вызывается только тогда, когда исключение выбрасывается наружу из деструктора. Если внутри деструктора находится try/catch блок, который ловит исключение и не пробрасывается дальше, это не приводит к прерыванию раскрутки стека внешнего исключения.
class ThrowCatchInDestructor {
public:
~ThrowCatchInDestructor() noexcept(false) {
try {
throw_int_func();
} catch (int i) {
std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "n";
}
}
private:
PrintInDestructor member_;
};
int main(int, char**) {
try {
ThrowCatchInDestructor good;
std::cerr << "ThrowCatchInDestructor instance createdn";
throw "BANG!";
} catch (int i) {
std::cerr << "Catched int exception: " << i << "n";
} catch (const char* s) {
std::cerr << "Catched const char* exception: " << s << "n";
} catch (...) {
std::cerr << "Catched unknown exceptionn";
}
return 0;
}
выводит
ThrowCatchInDestructor instance created
throw_int_func() invoked
Catched int in ~ThrowCatchInDestructor(): 1
~PrintInDestructor() invoked
Catched const char* exception: BANG!
Как же избегать неприятных ситуаций? В теории всё просто: никогда не бросать исключения в деструкторе. Однако, на практике красиво и изящно реализовать это простое требование бывает не так просто.
Сразу отмечу, что я не пытаюсь оправдать выбрасывание исключений из деструктора, и вслед за Саттером, Мейерсом и другими гуру C++ призываю вас постараться никогда этого не делать (по крайней мере, в новом коде). Тем не менее, программист в реальной практике вполне может столкнуться с legacy-кодом, который не так просто привести к высоким стандартам. Кроме того, зачастую описанные ниже методики могут пригодиться в процессе отладки.
Например, мы разрабатываем библиотеку с классом-обёрткой, инкапсулирующей работу с неким ресурсом. В соответствии с принципами RAII мы захватываем ресурс в конструкторе и должны освободить его в деструкторе. Но что если попытка освободить ресурс заканчивается ошибкой? Варианты решения этой проблемы:
Как же понять, находимся ли мы в данный момент в процессе раскрутке стека по исключению или нет? В C++ для этого есть специальная функция std::uncaught_exception(). С её помощью мы можем безопасно кидать исключение в обычной ситуации, либо делать что-либо менее правильное, но не приводящее к выбросу исключения во время раскрутки стека.
class ThrowInDestructor {
public:
~ThrowInDestructor() noexcept(false) {
if (std::uncaught_exception()) {
std::cerr << "~ThrowInDestructor() stack unwinding, not throwingn";
} else {
std::cerr << "~ThrowInDestructor() normal case, throwingn";
throw_int_func();
}
}
private:
PrintInDestructor member_;
};
int main(int, char**) {
try {
ThrowInDestructor normal;
std::cerr << "ThrowInDestructor normal destructionn";
} catch (int i) {
std::cerr << "Catched int exception: " << i << "n";
}
try {
ThrowInDestructor stack_unwind;
std::cerr << "ThrowInDestructor stack unwindingn";
throw "BANG!";
} catch (int i) {
std::cerr << "Catched int exception: " << i << "n";
} catch (const char* s) {
std::cerr << "Catched const char* exception: " << s << "n";
} catch (...) {
std::cerr << "Catched unknown exceptionn";
}
return 0;
}
Результат:
ThrowInDestructor normal destruction
~ThrowInDestructor() normal case, throwing
throw_int_func() invoked
~PrintInDestructor() invoked
Catched int exception: 1
ThrowInDestructor stack unwinding
~ThrowInDestructor() stack unwinding, not throwing
~PrintInDestructor() invoked
Catched const char* exception: BANG!
Обратите внимание, что функция std::uncaught_exception() является deprecated начиная со Стандарта C++17, поэтому чтобы скомпилировать пример, соответствующий ворнинг приходится подавлять (с.м. репозитарий с примерами из статьи [3]).
Проблема с этой функцией в том, что она проверяет находимся ли мы в процессе раскрутки стека по исключению. Но вот понять, вызван ли текущий деструктор в процессе раскрутки стека, с помощью этой функции невозможно. В результате, если происходит раскрутка стека, но деструктор какого-то объекта вызывается нормальным образом, std::uncaught_exception() всё равно вернёт true.
class MayThrowInDestructor {
public:
~MayThrowInDestructor() noexcept(false) {
if (std::uncaught_exception()) {
std::cerr << "~MayThrowInDestructor() stack unwinding, not throwingn";
} else {
std::cerr << "~MayThrowInDestructor() normal case, throwingn";
throw_int_func();
}
}
};
class ThrowCatchInDestructor {
public:
~ThrowCatchInDestructor() noexcept(false) {
try {
MayThrowInDestructor may_throw;
} catch (int i) {
std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "n";
}
}
private:
PrintInDestructor member_;
};
int main(int, char**) {
try {
ThrowCatchInDestructor stack_unwind;
std::cerr << "ThrowInDestructor stack unwindingn";
throw "BANG!";
} catch (int i) {
std::cerr << "Catched int exception: " << i << "n";
} catch (const char* s) {
std::cerr << "Catched const char* exception: " << s << "n";
} catch (...) {
std::cerr << "Catched unknown exceptionn";
}
return 0;
}
Результат:
ThrowInDestructor stack unwinding
~MayThrowInDestructor() stack unwinding, not throwing
~PrintInDestructor() invoked
Catched const char* exception: BANG!
В новом Стандарте C++17 на замену std::uncaught_exception() была представлена функция std::uncaught_exceptions() (обратите внимание на множественное число), которая вместо булевого значения возвращает количество активных в данный момент исключений (вот подробное обоснование [4]).
Вот как описанная выше проблема решается при помощи std::uncaught_exceptions():
class MayThrowInDestructor {
public:
MayThrowInDestructor() : exceptions_(std::uncaught_exceptions()) {}
~MayThrowInDestructor() noexcept(false) {
if (std::uncaught_exceptions() > exceptions_) {
std::cerr << "~MayThrowInDestructor() stack unwinding, not throwingn";
} else {
std::cerr << "~MayThrowInDestructor() normal case, throwingn";
throw_int_func();
}
}
private:
int exceptions_;
};
class ThrowCatchInDestructor {
public:
~ThrowCatchInDestructor() noexcept(false) {
try {
MayThrowInDestructor may_throw;
} catch (int i) {
std::cerr << "Catched int in ~ThrowCatchInDestructor(): " << i << "n";
}
}
private:
PrintInDestructor member_;
};
int main(int, char**) {
try {
ThrowCatchInDestructor stack_unwind;
std::cerr << "ThrowInDestructor stack unwindingn";
throw "BANG!";
} catch (int i) {
std::cerr << "Catched int exception: " << i << "n";
} catch (const char* s) {
std::cerr << "Catched const char* exception: " << s << "n";
} catch (...) {
std::cerr << "Catched unknown exceptionn";
}
return 0;
}
Результат:
ThrowInDestructor stack unwinding
~MayThrowInDestructor() normal case, throwing
throw_int_func() invoked
Catched int in ~ThrowCatchInDestructor(): 1
~PrintInDestructor() invoked
Catched const char* exception: BANG!
std::uncaught_exceptions() позволяет избежать вызова std::terminate(), но не помогает корректно обрабатывать множественные исключения. В идеале хотелось бы иметь механизм, который позволял бы сохранять все выброшенные исключения, а затем обработать их в одном месте.
Хочу ещё раз напомнить, что предложенный мной ниже механизм служит только для демонстрации концепции и не рекомендован к использованию в реальном промышленном коде.
Суть идеи состоит в том, чтобы ловить исключения и сохранять их в контейнер, а затем по одному доставать и обрабатывать. Для того, чтобы сохранять объекты исключений, в языке C++ есть специальный тип std::exception_ptr. Структура типа в Стандарте не раскрывается, но говорится, что это по сути своей shared_ptr на объект исключения.
Как же потом обработать эти исключения? Для этого есть функция std::rethrow_exception(), которая принимает указатель std::exception_ptr и выбрасывает соответствующее исключение. Нам нужно только поймать его соответствующей catch-секцией и обработать, после чего можно переходить к следующему объекту исключения.
using exceptions_queue = std::stack<std::exception_ptr>;
// Get exceptions queue for current thread
exceptions_queue& get_queue() {
thread_local exceptions_queue queue_;
return queue_;
}
// Invoke functor and save exception in queue
void safe_invoke(std::function<void()> f) noexcept {
try {
f();
} catch (...) {
get_queue().push(std::current_exception());
}
}
class ThrowInDestructor {
public:
~ThrowInDestructor() noexcept {
std::cerr << "~ThrowInDestructor() invokedn";
safe_invoke([]() {
throw_int_func();
});
}
private:
PrintInDestructor member_;
};
int main(int, char**) {
safe_invoke([]() {
ThrowInDestructor bad;
throw "BANG!";
});
auto& q = get_queue();
while (!q.empty()) {
try {
std::exception_ptr ex = q.top();
q.pop();
if (ex != nullptr) {
std::rethrow_exception(ex);
}
} catch (int i) {
std::cerr << "Catched int exception: " << i << "n";
} catch (const char* s) {
std::cerr << "Catched const char* exception: " << s << "n";
} catch (...) {
std::cerr << "Catched unknown exceptionn";
}
}
return 0;
}
Результат:
~ThrowInDestructor() invoked
throw_int_func() invoked
~PrintInDestructor() invoked
Catched const char* exception: BANG!
Catched int exception: 1
В примере выше для сохранения объектов исключений используется стек, однако обработка исключений будет производиться по принципу FIFO (т.е. логически это очередь — выброшенное первым исключение будет первым же и обработано).
Выбрасывание исключений в деструкторах объектов действительно является плохой идеей, и в любом новом коде я настоятельно рекомендую этого не делать, объявляя деструкторы noexcept. Однако, при поддержке и отладке legacy-кода может возникнуть потребность корректно обрабатывать исключения, выбрасываемые из деструкторов, в том числе и при раскрутке стека, и современный C++ предоставляет нам механизмы для этого. Надеюсь, представленные в статье идеи помогут вам на этом нелёгком пути.
Репозитарий с примерами из статьи [3]
Автор: igorsemenov
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/303027
Ссылки в тексте:
[1] Герб Саттер: https://www.ozon.ru/context/detail/id/1273200/
[2] N4713: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4713.pdf
[3] репозитарий с примерами из статьи: https://github.com/igor-semenov/code_samples/tree/master/destructive_exceptions
[4] обоснование: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4152.pdf
[5] Источник: https://habr.com/post/433944/?utm_campaign=433944
Нажмите здесь для печати.