The 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.
// Example 19-2(a): Why the wrong solution is wrong
//
U::~U()
{
try
{
T t;
// do work
}
catch( ... )
{
// clean up
}
}
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 acatch(
...)
block external to itself, up inU::~U()
. Note that none of this is materially different from the following:// Example 19-3: Variant, another wrong solution
//
Transaction::~Transaction()
{
if( uncaught_exception() )
{
RollBack();
}
else
{
// ... }
}
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:
// Example 19-3(a): Why the variant wrong solution
// is still wrong
//
U::~U() {
try {
Transaction t( /*...*/ );
// do work
} catch( ... ) {
// clean up
}
}
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 Immoral
The 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 differentways. 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 Solution
The right answer to the Example 19-1 problem is much simpler:
// Example 19-4: The right solution
//
T::~T() /* throw() */
{
// ... code that won't throw ...
}
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 ofT
, 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 theT
object and any resources it owns. That way, the calling code can callT::Close()
if it wants to detect hard errors, andT::~T()
can be implemented in terms ofT::Close()
plus atry
/catch
block:// Example 19-5: Alternative right solution
//
T::Close()
{
// ... code that could throw ...
}
T::~T() /* throw() */
{
try
{
Close();
}
catch( ... ) { }
}
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 functiontry
block.Guideline
Never allow an exception to be emitted from a destructor. Write destructors as though they had an exception specification
throw()
(whether the throw- specification actually appears in the code is a matter of personal taste).Guideline
If a destructor calls a function that might throw, always wrap the call in a
try
/catch
block that prevents the exception from escaping.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
uncaught_ exception()
. My advice: Don't use it.Item 20. An Unmanaged Pointer Problem, Part 1:
Parameter Evaluation
Difficulty: 6
Readers of Exceptional C++ and this book know that exception safety is anything but trivial. This Item points out an exception safety problem that was discovered fairly recently, and shows how best to avoid it in your own code.
1. In each of the following statements, what can you say about the order of evaluation of the functions
f
,g
, andh
and the expressionsexpr1
andexpr2
? Assume thatexpr1
andexpr2
do not contain more function calls.2.
4.
//
5.
f( expr1, expr2 );
6.
7.
// Example 20-1(b)
8.
//
9.
f( g( expr1 ), h( expr2 ) );
10. In your travels through the dusty corners of your company's code archives, you find the following code fragment: