|
Destructors in C++ provide a powerful foundation for resource management and are inherent to the RAII (Resource Acquisition Is Initialization) idiom. Some programmers even say that for exactly this reason the idiom should be actually called RRID - from Resource Release Is Destruction. Whatever is the actual name, the role of destructors in resource management is one of the cornerstones of C++.
There is a little problem with it - destructors have no easy way to report errors related to their own execution. Most programmers accept it and just silently ignore errors encountered when the given resource is released (like for example when closing a file handle), but others find it constraining and unnatural. In fact, destructors cannot use any of the two most common ways of reporting problems:
terminate()
. For this reason it is recommended and widely accepted that destructors should never throw any exceptions.There is a third alternative: modify some shared state so that interested parties can inspect it and learn about the outcome of the operation.
This approach is taken by old Unix functions that report problems using the errno
variable and is generally considered to be a bad practice.
The point of this article is to show that modification of the shared state, while considered to be a bad practice for regular functions, can be an interesting solution for destructors and can actually enable the exception chaining, which is known from "other programming languages" even if, interestingly, they happen to have no support for destructors anyway.
The exception chaining is based on the observation that the exception is not a single programming entity, but rather two things combined:
These two entities are tightly combined in languages like C++ or Java, where exceptions are first-class objects and can be arbitrarily heavy, but there are languages where the separation of these two is more visible. In particular, Ada and PL/SQL have exceptions that are not objects and do not, by themselves, carry any information other than the fact that they have occurred. The difference between these two language groups is even reflected in their terminology: in C++ and Java the exception is thrown (so presumably there must be some object that can be thrown and caught), whereas in Ada and PL/SQL the exception is risen and to do this the programmer does not have to provide any value of any type. It is still possible to associate some information with the exception occurrence, but the separation between these two aspects is clear.
Separating the exception occurrence from its value in C++ is an artificial trick that allows to manipulate the exception state in an arbitrary way, even when it is currently active. In particular, it allows to accumulate additional information to the already existing one if the new exception is to be signalled as part of stack unwinding.
In practical terms, the above concept can be implemented as follows.
The state of the exception, with operations to accumulate it and format the message are encapsulated by the chained_exception_state
class:
class chained_exception_state { public: static void set_active() { active_ = true; } static bool is_active() { return active_; } static void push(const string & message) { exception_messages_.push_back(message); } static string what() { string message; if (exception_messages_.empty() == false) { size_t i = exception_messages_.size() - 1; while (true) { message += exception_messages_[i]; if (i > 0) { message += "\n...caused by...\n"; --i; } else { break; } } } else { message = "No exception."; } return message; } static void clear() { active_ = false; exception_messages_.clear(); } private: static bool active_; static vector<string> exception_messages_; }; bool chained_exception_state::active_ = false; vector<string> chained_exception_state::exception_messages_;
The exception chained_exception
is used to signal the occurrence of a problem:
class chained_exception { public: string what() const { return chained_exception_state::what(); } };
Some macros can make it easier to glue things together:
#define CHAINED_THROW(msg) chained_exception_state::push(msg); \ if (chained_exception_state::is_active() == false) \ { \ chained_exception_state::set_active(); \ throw chained_exception(); \ } #define CHAINED_CLEAR chained_exception_state::clear()
The above scaffolding allows to report problems from destructors by simply adding the new problem description to the existing one and activating the exception if it is not yet active.
The following code is an example of chained exception usage:
class my_class { public: ~my_class() { cout << "executing destructor of my_class\n"; // ... // oops, something bad happened here cout << "throwing exception from destructor\n"; CHAINED_THROW("Problem in destructor!"); } }; int main() { try { my_class x; cout << "computing something\n"; // ... // oops, something bad happened here cout << "throwing exception from main\n"; CHAINED_THROW("Problem in computation!"); } catch (const chained_exception & e) { cerr << "\n\nException:\n" << e.what() << '\n'; CHAINED_CLEAR; } }
The above program prints:
$ g++ example.cpp $ ./example computing something throwing exception from main executing destructor of my_class throwing exception from destructor Exception: Problem in destructor! ...caused by... Problem in computation! $
Notes:
The exception chaining, as shown above, can be improved by:
The above scheme also has the following disadvantages:
active
flag. This can be solved by introducing reference counter to the chained_exception
class and appropriate logic in its own destructor, so that the shared state is cleared when the last copy of the given occurence is destroyed.CHAINED_THROW
anyway), but in more complex code it might become an issue.[*] Note: Shortly after publishing this article readers contributed the following links to relevant language extensions proposals:
These proposals are concerned mostly with the support for inter-thread exception propagation, but the underlying mechanisms can be also used to implement exception chaining.