Many C ++ experts (for example, the Sutter coat of arms ) teach us that throwing exceptions in destructors is bad, because you can get into the destructor while spinning a stack with an exception already thrown, and if another exception is thrown at that moment, std will be called :: terminate () . The C ++ 17 language standard (hereinafter I refer to the freely available version of the N4713 draft) tells us the following about this topic:
18.5.1 The std :: terminate () function [except.terminate]
1 substitution error handling techniques. [Note:
These situations are:
')
...
(1.4) when unwinding (18.2) terminates by throwing an exception, or
...
- end note]
Let's check on a simple example:
#include <iostream> class PrintInDestructor { public: ~PrintInDestructor() noexcept { std::cerr << "~PrintInDestructor() invoked\n"; } }; void throw_int_func() { std::cerr << "throw_int_func() invoked\n"; throw 1; } class ThrowInDestructor { public: ~ThrowInDestructor() noexcept(false) { std::cerr << "~ThrowInDestructor() invoked\n"; 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 exception\n"; } return 0; }
Result:
~ThrowInDestructor() invoked throw_int_func() invoked ~PrintInDestructor() invoked terminate called after throwing an instance of 'int' Aborted
Note that the PrintInDestructor destructor is still called, i.e. after throwing the second exception, the stack spinup is not interrupted. In the Standard (the same paragraph 18.5.1) on this subject the following is said:
2 ... In the situation where the handler is found,
it is not necessary to determine whether it’s std :: terminate () is called. In
the situation for the handler (18.3)
it is not-throwing exception specification (18.4),
unwound partially or unwound std :: terminate () is called ...
I tested this example on several versions of GCC (8.2, 7.3) and Clang (6.0, 5.0), everywhere the stack promotion continues. If you find a compiler where implementation-defined is different, please write about it in the comments.
It should also be noted that std :: terminate () is only called when promoting a stack when an exception is thrown out of the destructor. If there is a try / catch block inside the destructor that catches an exception and does not forward further, this does not interrupt the unwinding of the outer exception stack.
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 created\n"; 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 exception\n"; } return 0; }
displays
ThrowCatchInDestructor instance created throw_int_func() invoked Catched int in ~ThrowCatchInDestructor(): 1 ~PrintInDestructor() invoked Catched const char* exception: BANG!
How to avoid unpleasant situations? In theory, everything is simple: never throw exceptions to the destructor. However, in practice it is not so easy to beautifully and elegantly implement this simple requirement.
Immediately, I’m not trying to justify throwing exceptions from the destructor, and following Sutter, Meyers and other C ++ gurus I urge you to try never to do this (at least in the new code). However, a programmer in actual practice may well encounter a legacy code that does not easily lead to high standards. In addition, the techniques described below can often come in handy during the debugging process.
For example, we are developing a library with a wrapper class that encapsulates working with a certain resource. In accordance with the principles of RAII, we capture the resource in the constructor and must free it in the destructor. But what if the attempt to free a resource ends in error? Solutions to this problem:
How to understand whether we are currently in the process of promotion of the stack on the exception or not? In C ++, there is a special function std :: uncaught_exception () for this . With its help, we can safely throw an exception in a normal situation, or do something less correct, but not leading to an exception being thrown during the stack promotion.
class ThrowInDestructor { public: ~ThrowInDestructor() noexcept(false) { if (std::uncaught_exception()) { std::cerr << "~ThrowInDestructor() stack unwinding, not throwing\n"; } else { std::cerr << "~ThrowInDestructor() normal case, throwing\n"; throw_int_func(); } } private: PrintInDestructor member_; }; int main(int, char**) { try { ThrowInDestructor normal; std::cerr << "ThrowInDestructor normal destruction\n"; } catch (int i) { std::cerr << "Catched int exception: " << i << "\n"; } try { ThrowInDestructor stack_unwind; std::cerr << "ThrowInDestructor stack unwinding\n"; 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 exception\n"; } return 0; }
Result:
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!
Please note that the std :: uncaught_exception () function is deprecated starting with C ++ Standard 17, therefore, in order to compile an example, the corresponding worning has to be suppressed (see the repository with examples from the article ).
The problem with this function is that it checks whether we are in the process of spin stack promotion by exception. But it’s impossible to understand whether the current destructor was called during the stack promotion. As a result, if stack promotion occurs, but the destructor of some object is called normally, std :: uncaught_exception () will still return true .
class MayThrowInDestructor { public: ~MayThrowInDestructor() noexcept(false) { if (std::uncaught_exception()) { std::cerr << "~MayThrowInDestructor() stack unwinding, not throwing\n"; } else { std::cerr << "~MayThrowInDestructor() normal case, throwing\n"; 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 unwinding\n"; 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 exception\n"; } return 0; }
Result:
ThrowInDestructor stack unwinding ~MayThrowInDestructor() stack unwinding, not throwing ~PrintInDestructor() invoked Catched const char* exception: BANG!
In the new C ++ Standard Standard 17, std :: uncaught_exception () introduced the function std :: uncaught_exceptions () (pay attention to the plural), which instead of the boolean value returns the number of currently active exceptions (here is a detailed justification ).
Here's how the problem described above is solved with 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 throwing\n"; } else { std::cerr << "~MayThrowInDestructor() normal case, throwing\n"; 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 unwinding\n"; 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 exception\n"; } return 0; }
Result:
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 () avoids calling std :: terminate () , but does not help correctly handle multiple exceptions. Ideally, I would like to have a mechanism that allows you to save all the exceptions thrown, and then process them in one place.
I want to remind once again that the mechanism proposed by me below serves only to demonstrate the concept and is not recommended for use in a real industrial code.
The essence of the idea is to catch exceptions and store them in a container, and then one by one get and process. In order to save exception objects, in C ++ there is a special type of std :: exception_ptr . The type structure in the Standard is not disclosed, but it is said that this is essentially a shared_ptr on the exception object.
How then to process these exceptions? For this, there is a function std :: rethrow_exception () , which accepts a pointer std :: exception_ptr and throws the corresponding exception. We only need to catch it with the corresponding catch-section and process it, after which we can proceed to the next exception object.
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() invoked\n"; 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 exception\n"; } } return 0; }
Result:
~ThrowInDestructor() invoked throw_int_func() invoked ~PrintInDestructor() invoked Catched const char* exception: BANG! Catched int exception: 1
In the example above, the stack is used to save objects of exceptions, however, exception handling will be performed according to the FIFO principle (that is, logically this is the queue — the exception that was thrown first will be the first to be processed).
Throwing exceptions in the destructors of objects is indeed a bad idea, and in any new code I strongly recommend not to do this by declaring destructors noexcept . However, with the support and debugging of legacy code, there may be a need to correctly handle exceptions thrown from destructors, including during stack promotion, and modern C ++ provides us with mechanisms for this. I hope the ideas presented in this article will help you on this difficult path.
Source: https://habr.com/ru/post/433944/
All Articles