A virtual destructor in C++, when?
In today's post, I would like to explain a design rationale used in my post Understanding the inner workings of C++ smart pointers - The shared_ptr.
Keen readers spotted that in my implementation of ctrl_blk_base
, I didn't make the destructor virtual
. Here is the original code for easy reference:
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 |
|
You can see that there are two derived classes, ctrl_blk
and ctrl_blk_with_storage
, and we often got trained to make a destructor virtual
in a base class. For example, by Scott Meyers in Effective C++ Item 14: Make sure base classes have virtual destructors.
I think this is a good rule, but it is not perfect. A better wording is: Make sure base classes have virtual destructors if you want to support polymorphic delete.
A use-case for a polymorphic delete
Let me change the example to the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Above, I present a base class, IOPort,
and a single derived class, USBC,
as one example of a concrete port implementation. Further, IOPort
has a virtual
member function with Flush
. But in this initial example, the destructor in IOPort
isn't virtual
.
The correct, or better best, decision for or against a virtual
destructor is looking at the use-case IOPort
should support.
Here is one possible use case:
1 2 3 4 |
|
This is a very typical one, where we create a USBC
object but only store a pointer of the base class, sometimes a reference. In this case, the unique_ptr
will invoke the destructor of IOPort
upon destruction. Now, suppose USBC
contains data members with destructors. They will never get called. You address this use-case by making the destructor virtual
.
A use-case for a non-virtual
destructor
Yet, there is one other use case:
1 2 3 4 5 |
|
This time, I'm creating a USBC
object and storing it as such. Whether you now call functions expecting a USBC
or IOPort
object doesn't matter. I assume that the shared_ptr
deletes the object in the end. And it does so by using the destructor of USBC
. In this scenario, both IOPort
and USBC
can have data members with a destructor, and they will get called.
While my first example uses the powers of polymorphic delete, the second doesn't. The issue now is that all your colleagues have to understand this part and follow this design. Nobody can ever successfully use the destructor of IOPort
in the second example.
Well, the exact code I present here would work. Consider this a third use case. If your derived class has no data members with destructors, even with a non-virtual
destructor, you're safe.
Back to the ctrl_blk_base
. Remember, I'm no STL developer. I can't tell you precisely why this decision was made as is. Here is my rationale. Inside the STL, ctrl_blk_base
only is used for shared_ptr
. This is a very limited use case. In this specific use case, polymorphic destruction is never required. In fact, it would not work. If you now start, just for a possible event, to add a virtual
destructor that would impose a second entry in the virtual function table (vtable) for this class. This then comes with some extra costs for each required control block. For a case, we know precisely that it never happens.
The control block example is great for today's topic because it illustrates perfectly a use-case where you never want a virtual
destructor. Why? Because multiple shared_ptr
s reference a control block. The control block itself handles when it gets deleted. The only way to interact from the outside is release_shared
.
Another example of inheritance without a virtual
destructor are type traits. If you look at is_pointer
, for example. The type trait uses std::true_type
or std::false_type
as a base class. Yet none of them declares a virtual
destructor. First, they aren't expected to be used in a polymorphic way, and second, they don't have data members with destructors.
Summary
Getting the destructor right in C++ isn't easy. You can go the safe and simple way and make the destructor virtual
if there is at least one virtual
member function.
If you're striving for controlled performance and good modeling of your data type, you should take a closer look at the use cases it should support.
Andreas