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