Thursday 12 May 2005 — This is 18 years old. Be careful.
Back in December, I wrote about C++ constructor trivia. There, I said:
(*) C++ semantics state that if a constructor of a heap allocated object throws an exception, that the object’s destructor is called.
Joe Ganley wrote to point out that I had it wrong, and indeed I did. (I’ve used the linguist’s convention of starring sentences that are incorrect, though they do it for grammatical incorrectness.)
The correct statement of C++ semantics is:
- An object’s constructor will call constructors for each base class of the object.
- An object’s constructor will call constructors for each member of the object.
- If an exception is thrown at any point in the construction process, then a destructor will be called for each constructor that completed successfully.
6 printf("Base1 in.\n");
11 printf("Base1 out.\n");
20 printf("Base2 in.\n");
25 printf("Base2 out.\n");
34 printf("Member1 in.\n");
39 printf("Member1 out.\n");
43class Derived: Base1, Base2
48 printf("Throwing in Derived.\n");
49 throw "Hello";
54 printf("In ~Derived\n");
57 Member1 member1;
If you attempt to construct a Derived object, you will see this output:
Throwing in Derived.
Since the exception was thrown in the Derived constructor, the Derived destructor is not called. But destructors are called for each of the constructors that had completed (namely, Member1, Base2, and Base1).
C++ works very hard to keep track of exactly what class an object is. During the construction process, the class changes. When the Base1 constructor has finished, the object is a Base1. When the Base2 constructor has finished, it is also a Base2. By the time the Derived constructor is entered, it is considered a Derived, even though its constructor never finishes. This process of evolving the object up through its inheritance tree is the programming language equivalent of the now-discredited biological theory of recapitulation (famous for its tongue-twister slogan of Ontogeny recapitulates phylogeny).
During the destruction process, the whole thing happens in reverse. As each destructor finishes, the object changes classes reverting back to the mud from which it came.
C++ is very precise about the order of execution of all of these constructors. Base classes are constructed in the order they were declared, then members are constructed in the order they were declared. Destruction always happens in the reverse order.
By the way, this evolution of classes becomes even more important to understand when virtual function calls are considered. Exactly which function gets called for a virtual function depends on the class of the object. But the class is changing as the object is constructed and destructed. Calling virtual functions from constructors and destructors can be complicated because of this.
One last twist: if you declare a virtual pure method (with the horrid “= 0” syntax), you create a class which cannot be instantiated. So you’d think there would be no way to call the pure virtual. But think about the recapitulation of classes again. While an object is being constructed or destructed, it passes through those abstract classes. If one of those virtual functions is called when the object belongs to an abstract class, you will get the dreaded “pure virtual function call” error!
You've written a lot of great stuff about exceptions, but I can't help but see this particular piece of advice as off-beam. Over the years, I've seen a lot of misinformation on exceptions (Joel Spolsky is particularly unhelpful here at the moment) and ended up writing something on C++ and exceptions myself:
C++ constructors and exceptions, in particular, seem to generate an awful lot of misunderstanding. See the third section from the above link - Stroustrup is very much in favour of letting exceptions propagate from constructors, and is very much against the init() function approach. Obviously there are times when only an init() function will do, but these are actually very rare.
As far as the problem in your earlier column goes: an object does not truly exist until its constructor has been successfully completed [that's why destructors are only called on fully-constructed objects]. So you should not assign an object's "this" pointer to some external tracking variable except in the last line of its constructor. Or, even better, you could have a seperate routine call the constructor and then do the assign.
Anyway, as I say, I like a lot of what you've written and hope that this doesn't come across as too nit-picky.
I think you are correct: I didn't completely edit out the wrongness from December's post. It's OK to throw exceptions from constructors, you just have to understand what they will do. One of the things they can do is to give you objects in "impossible" states. I'll edit December's post a little more.
While that era was pre-exception handling support for most (or all?) of these compilers, it still mattered from the point of view of how calls to virtuals were handled during construction and destruction. ("Write once test everywhere" didn't originate with Java.)
Add a comment: