More C++ constructor trivia

Thursday 12 May 2005This is over 19 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:

  1. An object’s constructor will call constructors for each base class of the object.
  2. An object’s constructor will call constructors for each member of the object.
  3. If an exception is thrown at any point in the construction process, then a destructor will be called for each constructor that completed successfully.

For example:

 1class Base1
2{
3public:
4    Base1()
5    {
6        printf("Base1 in.\n");
7    }
8
9    ~Base1()
10    {
11        printf("Base1 out.\n");
12    }
13};
14
15class Base2
16{
17public:
18    Base2()
19    {
20        printf("Base2 in.\n");
21    }
22
23    ~Base2()
24    {
25        printf("Base2 out.\n");
26    }
27};
28
29class Member1
30{
31public:
32    Member1()
33    {
34        printf("Member1 in.\n");
35    }
36
37    ~Member1()
38    {
39        printf("Member1 out.\n");
40    }
41};
42
43class Derived: Base1, Base2
44{
45public:
46    Derived()
47    {
48        printf("Throwing in Derived.\n");
49        throw "Hello";
50    }
51
52    ~Derived()
53    {
54        printf("In ~Derived\n");
55    }
56
57    Member1 member1;
58};

If you attempt to construct a Derived object, you will see this output:

Base1 in.
Base2 in.
Member1 in.
Throwing in Derived.
Member1 out.
Base2 out.
Base1 out.

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!

» 3 reactions

Comments

[gravatar]
Ned, in your earlier article on constructors you seem to give the advice "try not to throw exceptions from constructors, or at least understand that they will destroy the object for you". You then say you moved most of the work of construction into a seperate init() function to fix the problem you encountered.

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:

http://www.arkestra.demon.co.uk/errors_cpp.html

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.

- Matt
[gravatar]
I don't consider it nit-picky at all. If I'm going to write something detailed, the details should be right.

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.
[gravatar]
It may be the case that recent generations of C++ compilers demonstrate consistent and predictable construction and destruction semantics however this of course wasn't always so. I can recall using MetaWare, Borland, MS, and various cfront C++ compilers in the mid to late 80's and early 90's where we noted implementation differences in how vtables and virtual function calls were handled during construction & destruction.

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:

Ignore this:
Leave this empty:
Name is required. Either email or web are required. Email won't be displayed and I won't spam you. Your web site won't be indexed by search engines.
Don't put anything here:
Leave this empty:
Comment text is Markdown.