You don’t always need a virtual desctructor¶
Let’s talk inheritance¶
Lets start off by doing some exercises with inheritance. I’ve written up 3 classes - a base, a derived, and a super. They inherit each other in the listed order. Lets take a look at them, shall we?
#include <iostream>
#include <memory>
struct Base
{
Base() {
std::cout << "--------------------" << std::endl;
std::cout << "ctor base" << std::endl;
}
~Base() {
std::cout << "~dtor base" << std::endl;
}
};
struct Derived : Base
{
Derived() { std::cout << "\t ctor derived" << std::endl; }
~Derived() { std::cout << "\t ~dtor derived" << std::endl; }
};
struct Super : Derived
{
Super() { std::cout << "\t\t ctor super" << std::endl; }
~Super() { std::cout << "\t\t ~dtor super" << std::endl; }
};
All i’ve done so far is create the 3 classes and add some logging so that we can tell when we enter the constructor and when we enter the destructor. Before we go any further, lets make sure we are on the same page in terms of how C++ constructors and destructors objects when there is multiple levels of inheritance available:
{
Super super{};
}
--------------------
ctor base
ctor derived
ctor super
~dtor super
~dtor derived
~dtor base
As you can see, we construct the lowest level first (Base
) and continue until we reach the highest level Super
and then when we destruct, we do it in the opposite order. Part of the reason this happens this way is so that we can construct and allocate the objects in the right dependency order, but we can discuss that at a later time.
Now that that’s out of the way, lets try an example that’s not so simple:
{
// Lets start by creating an instance of super, but capturing it as a base pointer
Base* base = new Super{};
delete base;
}
--------------------
ctor base
ctor derived
ctor super
~dtor base
You should immediately notice that the output doesn’t look like the same as what we saw earlier. What happened to:
~dtor super
~dtor derived
We didn’t clean up any of the derivations! This happens to be the unfortunate case because the type Base
doesn’t really know how to clean up the upper levels. How does Base
know if it of type Derived
or of type Super
or god forbid, something else?
So we have a problem. But what’s the normal solution? Well, it turns out that C++ provides us something called a virtual destructor
.
###saying Virtual Destructors
It looks just like a regular destructor, but happens to include the virtual
keyword. It looks something like:
struct SomeClass
{
// ... stuff ...
virtual ~SomeClass();
};
If you try googling “When to use virtual desctructor in C++”, you’ll get a lot of similar answers. Most likely the first you click will take you to this answer: https://stackoverflow.com/a/461224
Here is a quick blurb of the relevant bits:
Cool, so its basically saying we can fix our problem by changing our Base
class from:
struct Base
{
Base();
~Base();
};
to something more like:
struct Base
{
Base();
virtual ~Base();
};
So lets quickly sanity check that:
struct SafeBase
{
SafeBase() {
std::cout << "--------------------" << std::endl;
std::cout << "ctor safe base" << std::endl;
}
virtual ~SafeBase() {
std::cout << "~dtor safe base" << std::endl;
}
};
struct SafeDerived : SafeBase
{
SafeDerived() { std::cout << "\t ctor safe derived" << std::endl; }
~SafeDerived() { std::cout << "\t ~dtor safe derived" << std::endl; }
};
struct SafeSuper : SafeDerived
{
SafeSuper() { std::cout << "\t\t ctor safe super" << std::endl; }
~SafeSuper() { std::cout << "\t\t ~dtor safe super" << std::endl; }
};
{
SafeSuper super{};
}
{
SafeBase* base = new SafeSuper();
delete base;
}
--------------------
ctor safe base
ctor safe derived
ctor safe super
~dtor safe super
~dtor safe derived
~dtor safe base
--------------------
ctor safe base
ctor safe derived
ctor safe super
~dtor safe super
~dtor safe derived
~dtor safe base
Ay! Would you look at that. Just like the stackoverflow foretold. The prophacy holds! virtual ~Base
does the trick!
Lets make sure it works with our smart pointers too:
{
std::unique_ptr<Base> base = std::unique_ptr<Base>(new Super{});
}
{
std::unique_ptr<SafeBase> base = std::unique_ptr<SafeBase>(new SafeSuper{});
}
--------------------
ctor base
ctor derived
ctor super
~dtor base
--------------------
ctor safe base
ctor safe derived
ctor safe super
~dtor safe super
~dtor safe derived
~dtor safe base
Cool. That also checks out when we use std::unique_ptr
.
But this would be a pretty lame section if we just stopped there. I am here to tell you that you don’t actually always need it!
But you said we needed a virtual destructor!¶
Nah. Lets try another example:
{
std::shared_ptr<Base> base = std::make_shared<Super>();
}
--------------------
ctor base
ctor derived
ctor super
~dtor super
~dtor derived
~dtor base
Woah, wait what?!
We are using the old one. The one without the virtual destructor. We just saw that it doesn’t work. But why does it work if we use a shared_ptr
?
It turns out that one of the lesser known benefits of std::shared_ptr
is that it can remember your destructors. Not only that, it even lets you specify your own!
Your own, you say?
// Yup, we can actually just write a function that will be our custom destructor
void MyCustomDestructor(Super* s)
{
std::cout << "Hijacking the destructor without the class knowing!" << std::endl;
delete s;
}
If you didn’t know you could do this, well, now you do. This can be helpful when you want to take over certain bits before you clean up, maybe do some logging, some diagnostics, etc.
Once we have our custom destructor, we can just use it by doing:
{
std::shared_ptr<Base> base = std::shared_ptr<Base>(new Super{}, MyCustomDestructor);
}
--------------------
ctor base
ctor derived
ctor super
Hijacking the destructor without the class knowing!
~dtor super
~dtor derived
~dtor base
Isn’t that neat? So turns out we don’t need a virtual destructor
if we just use a std::shared_ptr
instead. With that said, std::shared_ptr
s can be overkill in most cases, so I generally try to avoid them as much as possible.
You might be wondering at this point: “What else can a shared pointer do? Why does this work? What’s it look like under the sheets?”
Well, head over to the next section to find out more!