GotW #80

Home Blog Talks Books & Articles Training & Consulting

On the
blog
RSS feed November 4: Other Concurrency Sessions at PDC
November 3
: PDC'09: Tutorial & Panel
October 26: Hoare on Testing
October 23
: Deprecating export Considered for ISO C++0x

This is the original GotW problem and solution substantially as posted to Usenet. See the book Exceptional C++ Style (Addison-Wesley, 2004) for the most current solution to this GotW issue. The solutions in the book have been revised and expanded since their initial appearance in GotW. The book versions also incorporate corrections, new material, and conformance to the final ANSI/ISO C++ standard (1998) and its Technical Corrigendum (2003).

Order, Order!
Difficulty: 2 / 10

Programmers learning C++ often come up with interesting misconceptions of what can and can't be done in C++. In this example, contributed by Jan Christiaan van Winkel, a student makes a basic mistake -- but one that many compilers allow to pass with no warnings at all.

Problem

JG Question

1. The following code was actually written by a student taking a C++ course, and the compiler the student was using issued no warnings about it. Indeed, several popular compilers issue no warnings for this code. What's wrong with it, and why?

  // Example 1
  //

  #include <string>

  using namespace std;

 

  class A

  {

  public:

    A( const string& s ) { /* ... */ }

    string f() { return "hello, world"; }

  };

 

  class B : public A

  {

  public:

    B() : A( s = f() ) {}

  private:

    string s;

  };

 

  int main()

  {

    B b;

  }

Guru Question

2. When you create a C++ object of class type, in what order are its various parts initialized? Be as specific and complete as you can.

Solution

1. [...]  What's wrong with [this code], and why?

  // ...

    B() : A( s = f() ) {}

  // ...

This line harbors a couple of related problems, both associated with object lifetime and the use of objects before they exist. Note that the expression "s = f()" appears as the argument to the A base subobject constructor, and hence will be executed before the A base subobject (or, for that matter, any part of the B object) is constructed.

First, this line of code tries to use the A base subobject before it exists. This particular compiler did not flag the (ab)use of A::f(), in that the member function f() is being called on an A subobject that hasn't yet been constructed. Granted, the compiler is not required to diagnose such an error, but this is the kind of thing standards folks call "a quality of implementation issue" -- something that a compiler is not required to do, but that better compilers could be nice enough to do.

Second, this line then goes on and merrily tries to use the s member subobject before it exists, namely by calling the member function operator=() is being called on a string member subobject that hasn't yet been constructed.

2. When you create a C++ object of class type, in what order are its various parts initialized? Be as specific and complete as you can.

The following set of rules is applied recursively:

bullet

First, the most derived class's constructor calls the constructors of the virtual base class subobjects. Virtual base classes are initialized in depth-first, left-to-right order.

bullet

Next, direct base class subobjects are constructed in the order they are declared in the class definition.

bullet

Next, (nonstatic) member subobjects are constructed, in the order they were declared in the class definition.

bullet

Finally, the body of the constructor is executed.

For example, consider the following code. Whether the inheritance is public, protected, or private doesn't affect initialization order, so I'm showing all inheritance as public.

  // Example 2
  //
  class B1 { };
  class V1 : public B1 { };
  class D1 : virtual public V1 { };

  class B2 { };
  class B3 { };
  class V2 : public B1, public B2 { };
  class D2 : public B3, virtual public V2 { };

  class M1 { };
  class M2 { };

  class X : public D1, public D2 { M1 m1_; M2 m2_; };

The inheritance hierarchy looks like this:

  B1      B1   B2
   |      |   /
   |      |  /
   |      | /
  V1      V2    B3
   |       |   /
   |v     v|  /
   |       | /
  D1       D2
    \     /
     \   /
      \ /
       X

The initialization order for a X object in Example 2 is as follows, where each constructor call shown represents the execution of the body of that constructor:

  first, construct the virtual bases:
    construct V1:
      B1::B1()
      V1::V1()
    construct V2:
      B1::B1()
      B2::B2()
      V2::V2()

  next, construct the nonvirtual bases:
    construct D1:
      D1::D1()
    construct D2:
      B3::B3()
      D2::D2()

  next, construct the members:
    M1::M1()
    M2::M2()

  finally, construct X itself:
    X::X()

This should make it clear why in Example 1 it's illegal to call either A::f() or the s member subobject's construct.

A(nother) Word About Inheritance

Of course, although the main point of this issue of GotW was to understand the order in which objects are constructed (and, in reverse order, destroyed), it doesn't hurt to repeated a tangentially related guideline:

Guideline: Avoid overusing inheritance.

Except for friendship, inheritance is the strongest relationship that can be expressed in C++, and should be only be used when it's really necessary. For more details, see also:

bullet

GotW #60: Exception-Safe Class Design, Part 2: Inheritance

bullet

Sutter's Mill #6: Uses and Abuses of Inheritance, Part 1

bullet

Sutter's Mill #7: Uses and Abuses of Inheritance, Part 2

bullet

The updated material in Exceptional C++ and More Exceptional C++

 

Copyright 2009 Herb Sutter