This is the original GotW problem and solution substantially as posted to Usenet. See the book More Exceptional C++ (Addison-Wesley, 2002) for the most current solution to this GotW issue. The solutions in the book have been revised and expanded since their initial appearance in GotW. The book versions also incorporate corrections, new material, and conformance to the final ANSI/ISO C++ standard. |
Uncaught Exceptions Difficulty: 6 / 10What is the standard function uncaught_exception(), and when should it be used? The answer given here isn't one that most people would expect. ProblemJG Question1. What does std::uncaught_exception() do? Guru Questions2. Consider the following code: T::~T() {
if( !std::uncaught_exception() ) {
// ... code that could throw ...
} else {
// ... code that won't throw ...
}
} Is this a good technique? Present arguments for and against. 3. Is there any other good use for uncaught_exception? Discuss and draw conclusions. Solution1. What does std::uncaught_exception() do? It provides a way of knowing whether there is an exception currently active. (Note that this is not the same thing as knowing whether it is safe to throw an exception.) To quote directly from the standard (15.5.3/1): The function bool uncaught_exception() returns true after completing evaluation of the object to be thrown until completing the initialization of the exception-declaration in the matching handler (_lib.uncaught_). This includes stack unwinding. If the exception is rethrown (_except.throw_), uncaught_exception() returns true from the point of rethrow until the rethrown exception is caught again.
As it turns out, this specification is deceptively close to being useful. 2. Consider the following code: T::~T() {
if( !std::uncaught_exception() ) {
// ... code that could throw ...
} else {
// ... code that won't throw ...
}
} Is this a good technique? Present arguments for and against. In short: No, even though it attempts to solve a problem. There are technical grounds why it shouldn't be used (i.e., it doesn't always work), but I'm much more interested in arguing against this idiom on moral grounds. Background: The ProblemIf a destructor throws an exception, Bad Things can happen. Specifically, consider code like the following: // The problem
//
class X {
public:
~X() { throw 1; }
};
void f() {
X x;
throw 2;
} // calls X::~X (which throws), then calls terminate() If a destructor throws an exception while another exception is already active (i.e., during stack unwinding), the program is terminated. This is usually not a good thing. The Wrong Solution"Aha," many people -- including many experts -- have said, "let's use uncaught_exception() to figure out whether we can throw or not!" And that's where the code in Question 2 comes from... it's an attempt to solve the illustrated problem: // The wrong solution
//
T::~T() {
if( !std::uncaught_exception() ) {
// ... code that could throw ...
} else {
// ... code that won't throw ...
}
} The idea is that "we'll use the path that could throw as long as it's safe to throw." This philosophy is wrong on two counts: first, this code doesn't do that; second (and more importantly), the philosophy itself is in error. The Wrong Solution: Why the Code Is UnsoundOne problem is that the above code won't actually work as expected in some situations. Consider: // Why the wrong solution is wrong
//
U::~U() {
try {
T t;
// do work
} catch( ... ) {
// clean up
}
} If a U object is destroyed due to stack unwinding during to exception propagation, T::~T will fail to use the "code that could throw" path even though it safely could. Note that none of this is materially different from the following: // Variant: Another wrong solution
//
Transaction::~Transaction() {
if( uncaught_exception() ) {
RollBack();
}
} Again, note that this doesn't do the right thing if a transaction is attempted in a destructor that might be called during stack unwinding: // Variant: Why the wrong solution is still wrong
//
U::~U() {
try {
Transaction t( /*...*/ );
// do work
} catch( ... ) {
// clean up
}
} The Wrong Solution: Why the Approach Is ImmoralIn my view, however, the "it doesn't work" problem isn't even the main issue here. My major problem with this solution is not technical, but moral: It is poor design to give T::~T() two different semantics, for the simple reason that it is always poor design to allow an operation to report the same error in two different ways. Not only does it complicate the interface and the semantics, but it makes the caller's life harder because the caller must be able to handle both flavours of error reporting -- and this when far too many programmers don't check errors well in the first place! The Right SolutionThe right answer to the problem is much simpler: // The right solution
//
T::~T() /* throw() */ {
// ... code that won't throw ...
} If necessary, T can provide a "pre-destructor" function (e.g., "T::Close()") which can throw and performs all shutdown of the T object and any resources that it owns. That way, the calling code can call T::Close() if it wants to detect hard errors, and T::~T() can be implemented in terms of T::Close() plus a try/catch block: // Alternative right solution
//
T::Close() {
// ... code that could throw ...
}
T::~T() /* throw() */ {
try {
Close();
} catch( ... ) {
}
} (Later note: See GotW #66 as to why this try block is inside the destructor body, and must not be a destructor function try block.) This nicely follows the principle of "one function, one responsibility"... a problem in the original code was that the same function was responsible for both destroying the object and final cleanup/reporting. From the GotW coding standards: - destructors: - never throw an exception from a destructor (Meyers96: 58-61) - if a destructor calls a function that might throw, always wrap the call in a try/catch block that prevents the exception from escaping - prefer declaring destructors as "throw()"
If this GotW issue hasn't convinced you to follow this coding standard, then maybe my other articles on exception safety will. See Scott Meyers' soon-to-be-published CD of his "Effective C++" and "More Effective C++" books, which also includes exception handling articles that I and Jack Reeves have written. In my section, note particularly the part entitled "Destructors That Throw and Why They're Evil" to see why you can't even reliably new[] an array of objects whose destructors can throw. (If you keep back issues of C++ Report, you can find the same articles in the October 1997 and November/December 1997 issues.) 3. Is there any other good use for uncaught_exception? Discuss and draw conclusions. Unfortunately, I do not know of any good and safe use for std::uncaught_exception. My advice: Don't use it. |