Solution

I l @ ve RuBoard

graphics/bulb.gif

I Can't Keep No Caught Exceptions

1. In Item 17 Example 17-1, if the A or B constructor throws an exception, [3] is it possible for the C constructor to absorb the exception and emit no exception at all?

[3] A double pun, can be sung to the chorus of the Rolling Stones' "Satisfaction" or to the opening bars of Pink Floyd's "Another Brick in the Wall, Part N. "

If we didn't consider the object lifetime rules, we might have tried something like the following:

 // Example 18-1(a): Absorbing exceptions? // C::C() try   : A ( /*...*/ )   // optional initialization-list   , b_( /*...*/ ) { } catch( ... ) {   // ? } 

How will the try block handler exit? Consider:

  • The handler cannot simply " return; " because that's illegal.

  • If the handler just says " throw; " it will rethrow whatever exception A::A() or B::B() originally emitted .

  • If the handler throws some other exception, that exception is emitted instead of whatever the base or member subobject constructor originally threw .

  • What's less obvious, but clearly stated in the C++ standard, is that if the handler does not exit by throwing an exception (either by rethrowing the original exception or by throwing something new), and control reaches the end of the catch block of a constructor or destructor, then the original exception is automatically rethrown as if the handler's last statement had been throw; .

Think about what this means: A constructor or destructor function try block's handler code must finish by emitting some exception. There's no other way. As long as you don't try to violate exception specifications, the language doesn't care what exception it is that gets emitted ”it can be the original one or some other translated exception ”but an exception there must be! It is impossible to keep any exceptions thrown by base or member subobject constructors from causing some exception to leak beyond their containing constructors.

In fewer words, it means that:

In C++, if construction of any base or member subobject fails, the whole object's construction must fail .

A constructor cannot possibly recover and do something sensible after one of its base or member subobjects' constructors throws. It cannot even put its own object into a "construction failed" state recognizable to the compiler. Its object is not constructed; it never will be constructed no matter what Frankensteinian efforts the handler attempts in order to breathe life into the nonobject. And whatever destruction can be done has already been done automatically by the language; that includes all base and member subobjects.

What if your class can honestly have a sensible "construction partially failed" state ”that is, it really does have some "optional" members that aren't strictly required and the object can limp along without them, possibly with reduced functionality? Then use the Pimpl idiom (described in Exceptional C++ [Sutter00] Items 27 “30) to hold the possibly-bad parts of the object at arm's length. For similar reasoning, see Exceptional C++ Items 31 “34 about abuses of inheritance. Incidentally, this "optional parts of an object" idea is another great reason to use delegation instead of inheritance whenever possible. Base subobjects can never be made optional because you can't put base subobjects into a Pimpl. [4]

[4] Convergence is funny sometimes. Long after I started pushing the Pimpl idiom and bashing needless inheritance, I kept coming across new problems that were solved by using Pimpl or removing needless inheritance, especially to improve exception safety. I guess it shouldn't have been a surprise, because it's just this whole coupling thing again. Higher coupling means greater exposure to failure in a related component. To this comment, Bobby Schmidt responded in private correspondence: "And maybe that's the core lesson to pull out of this ” we've really just rediscovered and amplified the old minimal-coupling-maximum-cohesion axiom ."

In the past, I've sometimes had a love/hate relationship with exceptions. Even so, I've always had to agree that exceptions are the right way to signal constructor failures, given that constructors cannot report errors via return codes (ditto for most operators). I have found the "if a constructor encounters an error, set a status bit and let the user call IsOK() to see if construction actually worked" method to be outdated , dangerous, tedious , and in no way better than throwing an exception. Ditto for its fraternal twin, the two-phase construction approach. I'm not the only one who views these approaches as odious. For a thorough debunking of these outmoded styles by none other than Stroustrup, including juicy sound bites like " bogus " and "this style is a relic of pre-exception C++," see section E.3.5 of [Stroustrup00].

A Step Toward Morality

Incidentally, this also means that the only (repeat only) possible use for a constructor function try block is to translate an exception thrown from a base or member subobject. That's Moral #1. Moral #2 says that destructor function try blocks are entirely usele ”

" ”But wait!" I hear someone interrupting from the middle of the room. "I don't agree with Moral #1. I can think of another possible use for constructor function try blocks ”to free resources allocated in the initializer list or in the constructor body!"

Sorry, no. Remember that once you get into your constructor try block's handler, any local variables in the constructor body are also already out of scope, and you are guaranteed that no base subobjects or member objects exist anymore, period. You can't even refer to their names . Either the parts of your object were never constructed, or those that were constructed have already been destroyed . So you can't be cleaning up anything that relies on referring to a base or member of the class (and anyway, that's what the base and member destructors are for, right?).

Aside: Why Does C++ Do It That Way?

To understand why it's good that C++ does it this way, let's put that restriction aside for the moment and imagine that C++ did let you mention member names in constructor try block handlers. Then imagine the following case, and try to decide: Should the handler delete t_ or z_ ? (Again, ignore for the moment that, in real C++, it can't even refer to t_ or z_ .)

 // Example 18-1(b): Very Buggy Class // class X : Y   {   T* t_;   Z* z_; public:   X()   try     : Y(1)     , t_( new T( static_cast<Y*>(this) )     , z_( new Z( static_cast<Y*>(this), t_ ) )   {     /*...*/   }   catch(...)  // Y::Y() or T::T() or Z::Z()                // or X::X()'s body has thrown   {     // Q: should I delete t_ or z_?     // (Note: NOT legal C++!)   } }; 

The first problem is that we cannot possibly know whether t_ or z_ were ever allocated. Therefore deleting either might not be safe.

Second, even if we did know that we had reached one of the allocations , we probably can't destroy *t_ or *z_ because they refer to a Y (and possibly a T ) that no longer exists, and they may try to use that Y (and possibly T ). Incidentally, this means that not only can't we destroy *t_ or *z_ , but they can never be destroyed by anyone .

If that didn't just sober you up, it should have. I have seen people write code similar in spirit to the above, never imagining that they were creating objects that, should the wrong things happen, could never be destroyed! The good news is that there's a simple way to avoid the problem. These difficulties would largely go away if the T* members were instead held by auto_ptr s or similar manager objects. (For more about the dangers of auto_ptr members and how to avoid them, turn to Items 30 and 31.)

Finally, if Y::~Y() can throw, it is not possible to reliably create an X object at any time! If you haven't been sobered yet, this should definitely do it. If Y::~Y() can throw, even writing " X x; " is fraught with peril. This reinforces the dictum that destructors must never be allowed to emit an exception under any circumstances, and writing a destructor that could emit an exception is simply an error. Destruction and emitting exceptions don't mix.

All right, enough about that. The preceding side discussion was just to get a better understanding of why it's good that the rules are as they are. In real C++, you can't refer to t_ or z_ inside the handler in the first place. I've refrained from quoting standardese so far, so here's your dose, from the C++ standard [C++98], clause 15.3, paragraph 10: "Referring to any non-static member or base class of an object in the handler for a function try block of a constructor or destructor for that object results in undefined behavior."

Morals About Function Try Blocks

Therefore, the status quo can be summarized as follows :

Moral #1: Constructor function try block handlers are only good for translating an exception thrown from a base or member subobject constructor (and maybe to do related logging or some other side effects in response to such failures). They are not useful for any other purpose.

Moral #2: Destructor function try blocks have little or no practical use, because destructors should never emit an exception. [5] Thus there should never be anything for a destructor function try block to detect that couldn't be detected with a normal try block. Even if there were something to detect because of evil code (namely a member subobject whose destructor could throw), the handler would not be useful for dealing with it because it couldn't suppress the exception. The best it could do is log something, or otherwise complain.

[5] Not even for logging or other side effects, because there shouldn't be any exceptions from base or member subobject destructors. Therefore, anything you could catch in a destructor function try block could be caught equally well using a normal try block inside the destructor body.

Moral #3: All other function try blocks have no practical use. A regular function try block can't catch anything that a regular try block within the function couldn't catch just as well.

Morals About Safe Coding

Moral #4: Always perform unmanaged resource acquisition in the constructor body, never in initializer lists. In other words, either use "resource acquisition is initialization" (thereby avoiding unmanaged resources entirely) or else perform the resource acquisition in the constructor body. [6]

[6] See [Stroustrup94] Section 16.5 and [Stroustrup00] Section 14.4.

For example, building on Example 18-1(b), say T was char and t_ was a plain old char* that was new[] 'd in the initializer list. Then there would be no way to delete[] it in the handler or anywhere else. The fix would be either to wrap the dynamically allocated memory resource (for example, change char* to string ) or to new[] it in the constructor body where it can be safely cleaned up using a local try block or otherwise.

Moral #5: Always clean up unmanaged resource acquisition in local try block handlers within the constructor or destructor body, never in constructor or destructor function try block handlers.

Moral #6: If a constructor has an exception specification, that exception specification must allow for the union of all possible exceptions that could be thrown by base and member subobjects . As Holmes might add, "It really must, you know." (Indeed, this is the way the implicitly generated constructors are declared; see [GotW] #69. [7] )

[7] Available online at http://www.gotw.ca/gotw/069.htm.

Moral #7: Use the Pimpl idiom to hold "optional parts" of a class's internals. If a constructor of a member object can throw but you can get along without said member, hold it by pointer and use the pointer's nullness to remember whether you've got one, as usual. Use the Pimpl idiom to group such "optional" members, so you have to allocate only once.

And finally, one last moral that overlaps with the rest but is worth restating in its own right:

Moral #8: Prefer using "resource acquisition is initialization" to manage resources. Really, really, really. It will save you more headaches than you can probably imagine, including hard-to-see ones, similar to some we've already dissected.

That's Just the Way It Is

From legality, we now turn to morality:

Justify your answer, explaining by example why this is as it should be .

The way the language works is entirely correct and easily defensible, once you think about the meaning of C++'s object lifetime model and philosophy.

A constructor exception must be propagated. There is no other way to signal that the constructor failed. Two cases spring to mind:

 // Example 18-1(c): Auto object // {   X x;   g( x ); // do something else } 

If X 's construction fails ”whether it's due to X 's own constructor body code or to some X base subobject or member object construction failure ”control must not continue within the scope of this code block. After all, there is no x object! The only way for control not to continue here is to emit an exception. Therefore, a failed construction of an auto object must result in some sort of exception, whether it is the same exception that caused the base or member subobject construction failure or some translated exception emitted from an X constructor function try block.

Similarly:

 // Example 18-1(d): Array of objects // {   X ax[10];   // ... } 

Say the fifth X object construction fails ”whether it's due to X 's own constructor body code failing, or to some X base or member subobject construction failing. Then control must not continue within the scope of this code block. After all, if you tried to continue, you'd end up with a "holey array" ”an array not all of whose objects really exist. (Even that would probably be better than leaving things in a holy disarray, but I digress.)

A Final Word: Failure-Proof Constructors?

2. What are the minimal requirements that A and B must meet in order for us to safely put an empty throw-specification on C 's constructor(s)?

Consider: Is it possible to write and enforce an empty throw-specification for a constructor of a class (like C in Item 17 Example 17-1) if some base or member constructor could throw? The answer is no; you could write the "I won't throw anything" empty throw-specification, but it would be a lie because there's no way to enforce it. To enforce a "throws nothing" guarantee for any function, we must be able to absorb any possible exceptions that come our way from lower-level code to avoid accidentally trying to emit them to our own caller. If you really want to write a constructor that promises not to throw, you can work around possibly-throwing member subobjects (for example, if you can hold them by pointer or Pimpl because they truly are optional), but you can't work around possibly-throwing base subobjects ”another reason to avoid unnecessary inheritance, which always implies gratuitous coupling.

For a constructor to have an empty throw-specification, all base and member subobjects must be known to never throw, whether they have throw-specifications that say so or not. An empty throw-specification on a constructor declares to the world that construction cannot fail. If for whatever reason it can indeed fail, then the empty throw-specification is inappropriate.

What happens if you wrote an empty throw-specification on a constructor, and a base or member subobject constructor really does throw? The short answer: "Go to terminate() . Go directly to terminate() . Do not pass try , do not collect $200." The slightly longer answer: The function unexpected() gets called, which has two choices ”to throw or rethrow an exception allowed by the exception specification (impossible, because it's empty and won't allow anything) or to call terminate() . terminate() , in turn, immediately aborts the program. [8] In automobile terms: screech, crunch.

[8] You can use set_unexpected() and set_terminate() to get your own handlers invoked, which gives you a chance to do a bit of extra logging or cleanup, but they'll end up doing much the same things.

Summary

A C++ object's lifetime begins only after its constructor completes successfully. Therefore, throwing an exception from a constructor always means (and is the only way of reporting) that construction failed. There is no way to recover from failed construction of a base or member subobject, so if construction of any base or member subobject fails, the whole object's construction must fail.

Avoid function try blocks, not because they're evil but because they offer few or no benefits over plain try blocks ”and when you're hiring, you'll find that more people understand the latter than the former. This follows the principles of picking the simplest solution that's effective, and of writing for clarity first. Constructor function try blocks are useful only to translate exceptions emitted from base or member constructors. All other function try blocks are rarely if ever useful at all.

Finally, as pointed out repeatedly in Exceptional C++ Items 8 to 19, use owning objects and "resource acquisition is initialization" to manage resources, and you'll usually avoid having to write try and catch at all in your code, never mind in function try blocks.

I l @ ve RuBoard


More Exceptional C++
More Exceptional C++: 40 New Engineering Puzzles, Programming Problems, and Solutions
ISBN: 020170434X
EAN: 2147483647
Year: 2001
Pages: 118
Authors: Herb Sutter

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net