|
Constructor Failures |
Aside: Convergence is funny sometimes. Long after I started pushing the Pimpl idiom and bashing needless inheritance, I kept on 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:
|
I've always had a love/hate relationship with exceptions, but 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.
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. Next, 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, namely to free resources allocated in the initializer list or in the constructor body!"
Sorry, nope. After all, 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 any more, 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?).
To see why it's good that C++ does it this way, let's put that restriction aside for the moment and imagine, just imagine, that C++ did let you mention member names in those 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 2(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++)
}
};
First, we cannot possibly know whether t_ or z_ were ever allocated, so neither delete could 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 auto_ptrs or similar manager objects.
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.
The above side discussion was to help better understand why the rules are as they are. After all, as noted, you can't even refer to t_ or z_ inside the handler anyway. I've refrained from quoting standardese elsewhere in this GotW, so here's your dose... from the C++ standard, 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.
Therefore the status quo can be summarized as follows:
Moral #1: Constructor function-try-block handlers have only one purpose -- to translate an exception. (And maybe to do logging or some other side effects.) They are not useful for any other purpose.
Moral #2: Since destructors should never emit an exception, destructor function-try-blocks have no practical use at all.[6] There should never be anything for them to detect, and even if there were something to detect because of evil code, the handler is not very useful for doing anything about it because it can not suppress the exception.
Moral #3: 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.
For example, building on Example 2(b), say T was char and t_ was a plain old char* that was new[]'d in the initializer-list; then in the handler there would be no way to delete[] it. The fix would be to instead either wrap the dynamically allocated memory resource (e.g., change char* to string) or new[] it in the constructor body where it can be safely cleaned up using a local try-block or otherwise.
Moral #4: 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 #5: 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 that the implicitly generated constructors are declared; see GotW #69.)
Moral #6: 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 or not, as usual. Use the Pimpl idiom to group such "optional" members so you only have to allocate once.
And finally, one last moral that overlaps with the above but is worth restating in its own right:
Moral #7: Prefer using "resource acquisition is initialization" to manage resources. Really, really, really. It will save you more headaches than you can probably imagine.
From legality, we now turn to morality:
Justify your answer, explaining by example why this is as it should be.
In short, 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, for there is no other way to signal that the constructor failed. Two cases spring to mind:
// Example 2(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 due to some X base subobject or member object construction failure -- control must not continue within f(). After all, there is no x object! The only way for control not to continue in f()'s body is to emit an exception. Therefore a failed construction of an auto object must result in some sort of exception, whether it be 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 2(d): Array of objects
//
{
X ax[10];
// ...
}
If the 5th X object construction fails -- whether it's due to X's own constructor body code failing, or due to some X base subobject or member object construction failing -- control must not continue within the scope. After all, if you tried to continue, you'd end up with an array not all of whose objects really exist.
We could slyly rephrase Question #2 as follows: Is it possible to write and enforce an empty throw-specification for a constructor of a class, if some base or member constructor could throw? After all, 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.
This, not coincidentally, brings us to the final question:
3. 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)?
Now that we've done all the hard work, this one's easy: For a constructor to have an empty throw-specification, all base and member subobjects must be known to never throw (whether they have throw-specs 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-spec isn't appropriate, that's all.
1. R. Schmidt. "Handling Exceptions in C and C++, Parts 14-16". (The link is to Part 15; you can navigate from there.)
2. For simplicity, I'm speaking only of the lifetime of an object of class type that has a constructor.
3. The inventor of the Python programming language?
4. H. Sutter. Exceptional C++ (Addison-Wesley, 2000).
5. R. Schmidt. Private correspondence, February 2, 2000.
6. Not even for logging or other side effects, because there shouldn't be any exceptions from base or member subobject destructors and 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.
7. A double pun, can be sung to the chorus of "Satisfaction" or to the opening bars of "Another Brick in the Wall, Part N."
Copyright © 2009 Herb Sutter |