Death and (virtual) destruction*

This time, we’ll have a more detailed look at one of those everybody-knows-that elements of C++ – virtual destructors.

More specifically, I want to reinforce under what circumstances you should make your destructor virtual; and when you don’t need to (despite what your compiler might say)

(*there’s no death)

My compiler is trying to help.  Probably.

When I build my code

class Base
{
public:
  virtual void op() { cout << "Base::op" << endl; }
};

My compiler emits a non-threatening, but not especially helpful, warning:

Class 'Base' has virtual method 'op' but non-virtual destructor

Of course, it’s only a warning so I can forget it, but if I’m a smart I should realise that if the compiler – which is focussed intently on producing an executing program – is warning me there’s something wrong then I should probably investigate.  Just in case.

The brute-force solution is to just add a virtual destructor to the class; and my warning goes away.  Fixed!

class Base
{
public:
  virtual void op() { cout << "Base::op" << endl; }

  virtual ~Base() = default;
};

The =default tells the compiler to provide the default implementation of this class’ destructor; which does nothing.

The rule for virtual destructors (first try)

We can now establish a simple rule for when you should add a virtual destructor to your class:

Virtual Destructor Rule (first attempt)

You must add a virtual destructor to your class if it has any other virtual methods 

This rule works; but it doesn’t really explain why we have to make destructors virtual.  To understand the logic beyond our rule (and hopefully refine it a little bit) we need to explore three other concepts:

  • Why do we have virtual functions?
  • Why should I write a destructor anyway?
  • Is a non-virtual destructor always necessary?

Why do we have virtual functions?

Virtual functions are an essential element of C++ polymorphism.  They give us dynamic dispatch of overridden functions.  That is, virtual functions allow the compiler (conceptually) at run-time to call (dispatch) a function based on the actual type of an object, when the client (caller) may only have a reference (or pointer) to a base type.

This mechanism enables the concept of Substitution – clients can hold a reference to a base class and derived-class objects can be substituted.

class Base
{
public:
  virtual void op() { cout << "Base::op" << endl; }
};


class Derived : public Base
{
public:
  virtual void op() { cout << "Derived::op" << endl; }
};


class Client
{
public:
  void doStuff() { server->op(); /* Polymorphic call */ }

private:
  friend void connect(Client& client, Base& base);
  Base* server { nullptr };
};

void connect(Client& client, Base& base) { client.server = &base; }


int main() 
{   
  Client client { };
  Base base { };
  
  connect(client, base);     // Substitute a Base object   
  client.doStuff();

  Derived derived { };
  
  connect(client, derived);  // Substitute a Derived object   
  client.doStuff(); 
}

The great benefit of creating substitutable objects is that modified or enhanced behaviour can be added to the code without modification to the client code.  As the client code hasn’t changed the substitution can be made at run-time – dynamically changing the client’s capabilities.

Note, if substitution is not used (as above) then the function call dispatch is static – (conceptually) fixed at compile time; in practice a normal function call, not a polymorphic call.

int main()
{
  Base base { };
  Derived derived { };

  base.op();   // Direct call to Base::op
  derived.op() // Direct call to Derived::op
}

We can revise our rule to be a bit more precise:

Virtual Destructor Rule (second attempt)

You must add a virtual destructor to your class if it, or any of its derived classes are used in a substitution hierarchy.

(The above might just seem like playing games with semantics, but it the distinction between having virtual functions and being used in a substitution hierarchy is important as we’ll see later)

Does this explain why destructors must be virtual as well?  Nope.

Why should I write a destructor?

The destructor is a special function, the complement of the constructor.  When an object reaches the end of its lifetime the destructor is called before the memory is deallocated.  The purpose of the destructor, then, is to act as a ‘clean-up’ function.

If you don’t write a destructor the compiler provides one for you that does nothing.  For many objects that’s absolutely fine:

  • Built-in types don’t need any special clean-up
  • Class types have their members automatically deallocated (and their destructors called) when they go out of scope.

However, there are occasions where the default behaviour is not sufficient:

  • A class manages the lifetime of another (dynamically-created) object
  • A class must leave a resource in a ‘safe’ state – closing a file, putting hardware into a quiescent mode, etc.

Since the destructor is automatically called it gives us a convenient place to put this behaviour to make sure it doesn’t get missed.  This general concept is known as Resource Management, and we explore it (in considerable detail) here.

So, if I’m not doing any resource management I don’t need to write a destructor.

If my class is to be part of a substitution hierarchy does this mean its destructor must be virtual?  Well, not necessarily.

The compiler ensures that, when a derived class object is destroyed, the base class destructor is automatically called.

class Base
{
public:
  virtual void op() { cout << "Base::op" << endl; }
  ~Base()           { cout << "Base::~Base" << endl; }
};


class Derived : public Base
{
public:
  virtual void op() { cout << "Derived::op" << endl; }
  ~Derived()        { cout << "Derived::~Derived" << endl; }
};


int main()
{
  Derived derived { };
}

 

The output will be

Derived::~Derived
Base::~Base

Notice, as with the virtual function call above, the call to the destructor is static dispatch.

If we decide to dynamically create our objects, and then use them in a substitution hierarchy, things change.

int main()
{
  Base* base { new Derived };  // NOTE:  Substitution!

  Base->op();                  // Polymorphic call

  delete base;                 // Calls Base::~Base ONLY!
}

In the above code we dynamically create a Derived object.  The new operator allocates memory for the Derived object then calls its constructor.  The address of the Derived object is used to initialise a Base pointer (yes, you should use a smart pointer type to manage the lifetime of the Derived object.  In this example I want to explicitly show what’s going on; and the result would be the same, anyway)

The call to delete does two things: call a destructor then deallocate the object’s memory.  The destructor invocation is just a function call.  As our destructor is not virtual the compiler will perform a static dispatch based on the type of the pointer – in our case, Base.  If our Derived class has a destructor implemented (performing some resource management) it will not be called!  This could have a deleterious effect on our system.

Making the destructor virtual ensures that a dynamic dispatch is performed on the destructor.  As before, the Derived destructor automatically calls its base class destructor.

We can now revise our rule to take the above into account

Virtual Destructor Rule (third attempt)

A class must declare a virtual destructor if:

  • It, or any of its derived classes, may perform resource management; AND
  • It is used in a substitution hierarchy; AND
  • It may be dynamically allocated

If any of the clauses can be guaranteed to NOT be true then the destructor does not need to be virtual.  There are several common cases where this could happen.  For example:

  • In many embedded systems dynamic allocation of objects is forbidden.
  • You are following the Rule of Zero

In the (general?) case where you cannot know how derived classes will be created or used this rule collapses to our first rule attempt.

A special case of inheritance

The general use of inheritance and virtual functions is building substitution hierarchies.  However, there is a special case use of inheritance which we should consider, known as the Class Adapter pattern.

The Class Adapter pattern uses private inheritance to allow a new interface to be added to existing code (if you want to study the Class Adapter pattern in more detail have a look here)

class Utility
{
public:
  void op1() { cout << "Utility::op1"  << endl; }
  void op2() { cout << "Utility::op2"  << endl; }
  void op3() { cout << "Utility::op3"  << endl; }
  void op4() { cout << "Utility::op4"  << endl; }
  void op5() { cout << "Utility::op5"  << endl; }

  // Virtual destructor required?
};


class Adapter : private Utility // Note PRIVATE inheritance!
{
public:
  // New interface
  //
  virtual void fn_A() { Utility::op1(); }
  virtual void fn_B() { Utility::op3(); }
  virtual void fn_C() { Utility::op5(); }
};

 

So, do we need to go back and modify Utility to add a virtual destructor?

Let’s apply our Virtual Destructor Rule and see.

Are Adapter objects going to be dynamically allocated?  We can’t possibly know, so let’s assume they could be.

Does the Utility class perform any resource management?  As the Utility class has no destructor declared we can assume the answer is no (or the Rule of Zero is in effect).  However, if any class inherits from Adapter they may perform resource management, so perhaps our Adapter should have a virtual destructor.

Is the Utility class, or any of its derived classes used in a substitution hierarchy?  Here the answer is a definite ‘no’.  Why?  Because the Adapter class changes the interface of the Utility class.  There is no way we can substitute an Adapter object for a Utility object in client code without substantially modifying the client to use the new interface.

Thus, when constructing Class Adapters it is not necessary for the adapted class to have a virtual destructor.

Conclusion

Just a reiteration of our rule for those who tl;dr this article:

The Virtual Destructor Rule

A class MUST declare a virtual destructor if (and only if):

  • It, or any of its derived classes, may perform resource management; AND
  • It is used in a substitution hierarchy; AND
  • It may be dynamically allocated

Virtual destructors seem to cause consternation in programmers.  If you’re in the mood for losing a couple of hours just search for “inherit from std::vector”.  Here are a couple to get you started:

https://stackoverflow.com/questions/4353203/thou-shalt-not-inherit-from-stdvector

https://stackoverflow.com/questions/1647298/why-dont-stl-containers-have-virtual-destructors

Apply the above rule and you won’t go too far wrong.

 

More information

To learn more about Feabhas’ C++ training courses, click here.

Glennan Carnie
Dislike (0)
Website | + posts

Glennan is an embedded systems and software engineer with over 20 years experience, mostly in high-integrity systems for the defence and aerospace industry.

He specialises in C++, UML, software modelling, Systems Engineering and process development.

About Glennan Carnie

Glennan is an embedded systems and software engineer with over 20 years experience, mostly in high-integrity systems for the defence and aerospace industry. He specialises in C++, UML, software modelling, Systems Engineering and process development.
This entry was posted in C/C++ Programming, Design Issues and tagged , , , , , , , , . Bookmark the permalink.

9 Responses to Death and (virtual) destruction*

  1. Nice article, but I think that the rule of "It, or any of its derived classes, may perform resource management; " has nothing to do with the problem. Using the default destructor does not imply that the destructor does not have to be called when destroying the object. Otherwise you get no destruction of derived class members, and derived class's other base classes in case of multiple inheritance.

    Like (1)
    Dislike (0)
  2. Fair argument Eric; thanks

    Like (0)
    Dislike (0)
  3. Those three clauses do not yet capture the essence. If you have all of these - including dynamic allocation - but the thing calilng its destructor knows the actual type to destruct, you *still* do not need a virtual destructor. Case in point: std::make_shared and std::make_unique. If you use these with a class that owns resources, is dynamically allocated (with these) and then stored as a shared or unique pointer to its base, it will *still* call the right deletion function.

    > You are following the Rule of Zero

    This is not enough either. If you own objects that own resources you don't need to implement a destructor, but if your default destructor is not virtual their destructor will not be called.

    Like (2)
    Dislike (0)
  4. Great use case Peter! Thanks for that.

    Another good reason to prefer smart pointers over 'raw' memory management.

    Like (0)
    Dislike (0)
  5. Could we use smart pointers with a provided deleter to work around the missing virtual destructor?

    Like (0)
    Dislike (0)
  6. See Peter's post (above)

    Like (0)
    Dislike (0)
  7. As I understand Peter's post we don't need to provide a deleter. It's really cool.

    Like (0)
    Dislike (0)
  8. Kai Stuhlemmer says:

    Be cautious with unique_ptr as it does not store the pointer to deleter! Even if You use make_unique, as soon as You move it to a unique_ptr of base class the derived deleter is no longer called. In this regard (and many others) unique_ptr behaves exactly as raw pointer.

    Example: https://coliru.stacked-crooked.com/a/8972a4a4d94be898

    The same use case with shared_ptr is safe, because it does store a pointer to deleter.

    Like (0)
    Dislike (0)
  9. Beavith says:

    delete base; // Calls Base::~Base ONLY!

    NOOOOOO!!!! This causes undefined behavior. This may include calling the base class destructor. This may include calling a wrong deallocation function. This may cause anything else. Undefined behavior means no expectations.

    Like (0)
    Dislike (0)

Leave a Reply