Have you ever seen class inheritance (mostly in legacy software) where each child class declares a virtual destructor even it’s empty? This is absolutely not necessary and should be avoided!

The only class in a inheritance hierarchy that needs a virtual destructor - even when its empty - is the abstract base class every other class in the chain inherits from.

Lets build a chain with class A as parent, with class B as its subclass and class C as subclass from B. The class Member is to signal when a member has been con-/destructed and it’s used in class B only:

[A] <-- [B] <-- [C]:

# include <iostream>

class Member {
public:
  Member(void) {
    std::cout << "Member()" << std::endl;
  }
  ~Member(void) {
    std::cout << "~Member()" << std::endl;
  }
};

class A {
public:
    A(void) {
        std::cout << "A()" << std::endl;
    }
    
    virtual ~A(void) {
        std::cout << "~A()" << std::endl;
    };
    
};

class B: public A {
    Member member;
public:
    B(void) {
        std::cout << "B()" << std::endl;
    }
};

class C : public B {
public:
  C(void) {
    std::cout << "C()" << std::endl;
  }
  ~C(void) {
    std::cout << "~C()" << std::endl;
  }
};

Class B follows the rule of zero since no explicit resource initialization is necessary. To see construction / destruction of instances from B I added a Member instance as member of class B.


So lets have a look at the first example, a simple instantiation of C on the stack:

{
    C c{};
}

Not really suprisingly this generates the following output:

A()                 # base class `A` gets constructed
Member()            # initialization of class `B` starts here, at first all members are constructed
B()                 # then the constructor of `B` gets called
C()                 # at least `C` gets constructed.
~C()                # here we left the scope and `C` gets destructed first
                    # if we would have declared a destructor for `B`, this would have been called at this point
~Member()           # now all members of `B` will be destroyed
~A()                # at least `A` gets destructed

Now lets check what happens if we construct the instances on the heap.. I’ll use new / delete explicitly for better understanding, but of course normally I would use std::unique_ptr here.

{
    std::unique_ptr<A> = std::make_unique<C>();
}

is exactly the same as

{
    C *c = new C{};
    A *a = c;
    delete a;
}

(except when it comes to resource safety, by the way).

Lets run and check the output:

A()
Member()
B()
D()
~D()
~Member()
~A()

As expected this behaves exactly as the stack allocation, although we call the destructor for A, not for C. Casting c to a pointer of type B and deleting it would deliver the same result.


But what if class A destructor wouldn’t be declared virtual? Lets change A and remove the virtual keyword.

class A {
public:
    A(void) {
        std::cout << "A()" << std::endl;
    }
    
    ~A(void) {
        std::cout << "~A()" << std::endl;
    };
};

Now when you compile this your compiler may warn you that you didn’t declare the base class constructors as virtual. If class A would be abstract by declaring a pure virtual method any compiler should warn you (if not, use another!).

Anyway, lets look at the output for stack allocation:

A()
Member()
B()
C()
~C()
~Member()
~A()

Looks like the same as in the example with the virtual A destructor. This is because the compiler knows about constructing / destructing an instance of C and therefore the inheritance hierarchy.

Now the heap example again:

{
    C *c = new C{};
    A *a = c;
    delete a;
}

and its output:

A()
Member()
B()
C()
~A()

Oh, what happened? By deletion of a (of type A) the destructor of A gets called. But now it isn’t virtual, so no runtime lookup at the virtual function table is done. This leads to not performing the correct cleanup order!

So ensure making the destructor of your parent superclass always virtual, whilst leaving it out for subclasses if it is empty.

And why not leaving empty sublcass destructors? Because than you could violate the rule of three or even zero - and would not profit from it. This says that as soon as you provide a user defined destructor - even if its empty or =default, the compiler won’t add the implicit move constructor or move assign operator for you and you also have to provide a user defined copy constructor and assignment operator for this class.