Not all the parts of a C++ class have the same lifespan. Distinct steps are executed in sequence in the order to build or cleanup a C++ class instance.

Scope

An instance of a class lives from when it’s constructed, until it’s destructed. Generally the scope controls the duration of the lifetime and the bodies of the constructor and the destructor define the actions to perform at the start and the end of the lifetime.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void some_fn()
{
  SomeClass x; // x is constructed here
  SomeClass y; // y is constructed here
  // more code here
  {
    SomeClass z; // z is constructed here
    // more code here
    // z is destructed when we exit this block
  }
  // even more code here
  // x and y are destructed when we exit this block
  // y is destructed first, then x
}

The rationale for destructing y and then x, in the reverse order in which they were constructed is that y could depend on x, but not the other way around, so y has to be destructed first. Of course if the compiler determines that there is no such dependency, and no side effects, it is free to re-order assembly instructions (the as-if rule).

Lifetime

Say we have a SomeClass derived from Base1 and Base2, with two member variables a and b, and with at least a virtual method that overrides the a virtual method in Base1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class SomeClass :
  // order of base classes matters
  public Base1,
  public Base2
{
  // order of member variables matters
  SomeType a;
  SomeOtherType b;

  // virtual function overrides method from Base1
  void some_method() override;

public:
  SomeClass()
    // initialization here (before the body)
  {
    // constructor body here
  }

  ~SomeClass()
  {
    // destructor body here
  }
};

The lifetime of the various parts of such a class is illustrated on the diagram below. The full loop indicates the happy path: all the construction steps succeed, the instance is used, then the destruction steps take place in reverse order. However if something fails during the construction, the code will take the shortcut indicated by the slanted arrows, reverting the work done up to that point, in which case the caller needs to deal with the exception and can’t use the instance.

allocate memory construct base Base1 construct base Base2 construct member a construct member b init vtable pointer SomeClass constructor body usage deallocate memory destruct base Base1 destruct base Base2 destruct member a destruct member b revert vtable pointer SomeClass destructor body

First the memory must be allocated, either on the stack, or on the heap. The memory required for an instance of SomeClass is the sum of:

  • The sum of of the base classes sizes. This also includes the pointer to the virtual functions table (the vtable); most of the C++ implementations use this technique for virtual functions
  • The size of the member variables a and b
  • Potentially additional bytes due to alignment and padding issues and potentially less bytes if any of base classes are empty

The constructors for the base classes are invoked, one by one, in the order in which they were inherited. In this case the constructor for the Base1 class is invoked first, followed by the constructor for the Base2 class. The constructor for SomeClass can pass additional arguments to a base class in the constructor’s initialization list, for example:

1
2
3
4
5
6
7
SomeClass::SomeClass(int i, int j, int k) :
  // initialization list
  Base1{ i, j },
  Base2{ k }
{
  // constructor body
}

The member variables are then initialized in the order in which they are declared, not in the order they appear (if they appear) in the constructor’s initialization list, though it is good practice to put them in the same order in the initialization list, if possible.

1
2
3
4
5
6
7
8
9
SomeClass::SomeClass(int i, int j, int k) :
  // initialization list
  Base1{ i, j },
  Base2{ k },
  a{ k },
  b{}
{
  // constructor body
}

Before the body of the SomeClass constructor, the pointer to the virtual functions table is set to point to the SomeClass vtable. Just before that, if virtual functions are called, they would be the base class ones.

Finally the body of the SomeClass constructor is invoked.

If something goes wrong at any of steps during construction, the compiler will take a shortcut indicated by the slanted arrows, and undo the steps up to that point in reverse order, before throwing an exception.

For example if the constructor of a class fails, then the destructor is not called (the rationale for that is that the object was not constructed), but member variables are destructed, base classes are destructed and memory is deallocated.

Another example is that if the constructor of the second member variable fails, then the second member variable does not need to (will not) be destructed, but the first variable is destructed, same for base classes, memory is deallocated.

Example

For example an incorrect naive implementation of a class that handles to FILE pointers might look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class bad_two_files
{
  FILE * src;
  FILE * dst;

public:
  bad_two_files(const char * src_name, const char * dst_name) :
    src{},
    dst{}
  {
    src = ::fopen(src_name, "rb");
    if ( ! src)
    {
      throw std::runtime_error("Failed to open file");
    }
    dst = ::fopen(dst_name, "wb");
    if ( ! dst)
    {
      throw std::runtime_error("Failed to open file");
    }
  }

  ~bad_two_files()
  {
    if (src)
    {
      ::fclose(src);
    }
    if (dst)
    {
      ::fclose(dst);
    }
  }

  bad_two_files(const bad_two_files &) = delete;
  bad_two_files & operator= (const bad_two_files &) = delete;

  bad_two_files(bad_two_files && other) :
    src{ other.src },
    dst{ other.dst }
  {
    other.src = nullptr;
    other.dst = nullptr;
  }

  bad_two_files & operator= (bad_two_files && other)
  {
    if (this != &other)
    {
      if (src)
      {
        ::fclose(src);
      }
      if (dst)
      {
        ::fclose(dst);
      }
      src = other.src;
      dst = other.dst;
      other.src = nullptr;
      other.dst = nullptr;
    }
    return *this;
  }
};

The problem is that if the two_files constructor fails while opening the destination file, the destructor is not called, and the source file is not closed. The smell in this case is that the two_files class tries to directly manage two resources.

The better option is to have a class that manage a single resource, and have two_files composed using two member variables of that class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class file
{
  FILE * f;

public:
  file(const char * file_name, const char * mode)
  {
    f = ::fopen(file_name, mode);
    if ( ! f)
    {
      throw std::runtime_error("Failed to open file");
    }
  }

  ~file()
  {
    if (f)
    {
      ::fclose(f);
    }
  }

  file(const file &) = delete;
  file & operator= (const file &) = delete;

  file(file && other) noexcept :
    f{ other.f }
  {
    other.f = nullptr;
  }

  file & operator= (file && other) noexcept
  {
    if (this != &other)
    {
      if (f)
      {
        ::fclose(f);
      }
      f = other.f;
      other.f = nullptr;
    }
    return *this;
  }
};

class two_files
{
  file src;
  file dst;

public:
  two_files(const char * src_name, const char * dst_name) :
    src{ src_name, "rb" },
    dst{ dst_name, "wb" }
  {
  }
};

This way, if opening the destination file fails, then creating the instance of two_files fails, but not before the source file is closed as part of it’s destructor. It also comes to pretty much the same number of code lines (fewer lines of code in fact).

Summary

Understand and take advantage of the initialization sequence for C++ classes.

NOTE: When using virtual inheritance like in struct Derived: virtual Base, things are more complicated, but that’s a whole separate topic on a C++ language feature that’s not often used.