Virtual Destructors

2021/04/24

Categories: C++ dynamic polymorphism

Any class that has a virtual function (including an interface class with only virtual functions) should implement a virtual destructor [1]. This requirement can seem somewhat odd at first, and this post briefly explains why this requirement exists.

Object Construction

First, let’s take a quick look at object construction. Assume we have a simple inheritance hierarchy where we have some base class (called Base) with a derived class (called Derived). When a Derived object is constructed, the Base constructor is called, followed by the Derived constructor.

The example below shows this. (Note that destructors have been eliminated as they are discussed later).

#include <iostream>
#include <memory>

class Base {
public:
  Base() : some_string("Str") {
    std::cout << "Base constructor: " << some_string << "\n";
  }
  std::string some_string;
};

class Derived : public Base {
public:
  Derived() { std::cout << "Derived constructor: " << some_string << "\n"; }
};

int main() { std::unique_ptr<Base> foo = std::make_unique<Derived>(); }

The constructor Base() initializes some string (some_string) via a member initializer list. It will then print the string in the constructor. The constructor for Derived() simply echoes the string once again.

Base constructor: Str
Derived constructor: Str

This works exactly as we expect. We expect that everything from the Base class is available to us in the Derived class, and this ordering (inherited classes are constructed first) enables this.

Virtual Destructor

Object destruction is somewhat more complicated because we are often working through a pointer to the base class. Consider the simple example below.

#include <iostream>
#include <memory>

class Base {
public:
  Base() { std::cout << "Base constructor\n"; }
  ~Base() { std::cout << "Base destructor\n"; } // BAD: not virtual
};

class Derived : public Base {
public:
  Derived() { std::cout << "Derived constructor\n"; }
  ~Derived() { std::cout << "Derived destructor\n"; }
};

int main() {
  std::unique_ptr<Base> foo = std::make_unique<Derived>();
}

The output of this is:

Base constructor
Derived constructor
Base destructor

The derived destructor is never called. Foo is a Base pointer, so when it goes out of scope (immediately after construction), the destructor ~Base() is called.

This can be corrected by making the destructor ~Base() virtual, which results in the following output:

Base constructor
Derived constructor
Derived destructor
Base destructor

Now, the program is able to recognized that the destructor is virtual, and has two implementations. Specifically, it is implemented for both ~Base() and ~Derived(). Each is called in the reverse order of object construction.

Subtle Differences Between Deleting a std::unique_ptr and std::shared_ptr

The above behaviour — where we fail to call the ~Derived() destructor — is seen with a std::unique_ptr but not a std::shared_ptr. That is, had you rewritten main() to only ever use shared_ptr, the object would have been destructed in the correct order.

This will be discussed in a future post, and is the result of subtle differences in the standard’s definition of the destructor between the two types.

References

[1] B. Stroustrup and H. Sutter (editors), “C.127 A Class with a virtual function should have a virtual or protected destructor”, C++ Core Guidelines.

>> Home