I l@ve RuBoard |
SolutionRecapping uncaught_exception()
The standard uncaught::exception() function provides a way of knowing whether there is an exception currently active. An important thing to note is 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):
As it turns out, this specification is deceptively close to being useful. Background: The Problem with Throwing DestructorsIf a destructor throws an exception, Bad Things can happen. Specifically, consider code like the following:
If a destructor throws an exception while another exception is already active (that is, during stack unwinding), the program is terminated. This is usually not a good thing. For more information, see also Exceptional C++ [Sutter00] Item 16, which discusses "Destructors That Throw and Why They're Evil." The Wrong Solution"Aha," many people, including many experts, have said, "let's use uncaught_ exception() to figure out whether we can throw!" And that's where the code in Question 2 comes from. It's an attempt to solve the illustrated problem:
Is this a good technique? Present arguments for and against. In short: No, this is not a good technique, even though it attempts to solve a problem. There are technical grounds why this technique shouldn't be used. But I'm more interested in arguing against this idiom on moral grounds. The idea behind the idiom in Example 19-2 is simple: 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 important, in my opinion), the philosophy itself is in error. Let's investigate these two points separately. Why the Wrong Solution Is UnsoundThe first problem is that the idiom in Example 19-2 won't actually work as intended in some situations. That's because it can end up using the path that doesn't throw even when it would be safe to throw.
If a U object is destroyed because of stack unwinding during exception propagation, T::~T() will fail to use the "code that could throw" path even though it safely could. T::~T() doesn't know that in this case it's already protected by a catch(...) block external to itself, up in U::~U(). Note that none of this is materially different from the following:
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:
So Example 19-2 doesn't work the way it's intended to work. That's fine, but it's not the main issue. Why the Wrong Solution Is ImmoralThe second and more fundamental problem with this solution is not technical, but moral. It is poor design to give T::~T() two different "modes" of operation for its error reporting semantics. The reason is that it is always poor design to allow an operation to report the same error in two different ways. Making the interface not merely modal, but modal in a way that the calling code can't easily control or account for, has two major failings. It complicates the interface and the semantics. And it makes the caller's life harder because the caller must be able to handle both flavors of error reporting�and this when far too many programmers don't check errors well in the first place. Sometimes, when I'm driving my car, I find myself behind another driver who's partly in one lane and partly in the next. After several seconds have passed and the driver is still (from my point of view) behaving erratically, I feel the urge to roll down the window and call out, in a loud, but kind, thoughtful, and caring voice: "C'mon, buddy! Pick a lane! Any lane!" As long as the other driver continues to straddle the line, I have to be ready to handle the possibility of his moving into either lane at any time. This is not only annoying, but it slows down my own progress. Sometimes, when we're writing code, we find ourselves using another programmer's class or function that has a schizophrenic interface, such as indecisively trying to report the same failure in more than one way, instead of just picking one set of semantics and running with those semantics consistently. We should all feel the urge to walk over to the offender's office or cube and help this poor unenlightened person to make a decision that would simplify life for us and other users. The Right SolutionThe right answer to the Example 19-1 problem is much simpler:
Example 19-4 demonstrates how to make a design decision instead of waffling. Note that the throw() throws-nothing exception specification is only a comment. That's the style I've chosen to follow, in part because it turns out that exception specifications confer a lot less benefit than they're worth. Whether or not you decide to actually write the specification is a matter of taste. The important thing is that this function won't emit an exception. For a discussion about the possibility of exceptions thrown by destructors of members of T, turn to Item 18. If necessary, T can provide a "pre-destructor" function (for example, T::Close()), which can throw and which performs all shutdown of the T object and any resources 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:
This nicely follows the principle of "one function, one responsibility." A problem in the original code was that it had the same function responsible for both destroying the object and final cleanup/reporting. See also Items 17 and 18 about why this try block is inside the destructor body and should not be a destructor function try block. Guideline
Guideline
Unfortunately, I do not know of any good and safe use for uncaught_ exception(). My advice: Don't use it. |
I l@ve RuBoard |
No comments:
Post a Comment