Featured Blog | This community-written post highlights the best of what the game industry has to offer. Read more like it on the Game Developer Blogs or learn how to Submit Your Own Blog Post
Virtual mess
"What are virtual destructors and virtual constructors for?" Who here hasn't heard that trick question at their job interview, please raise their hand. This innocent little sentence hides a boogey man though, and I'm here to lure it out of the closet.
It’s all about the design
This is going to be a not-so-short rant on a subject that’s been bugging me since I first went to a job interview.
I’m a C++ programmer by heart and profession. And what it means is that basically at every interview I get asked one and the same question:
“What are virtual destructors and virtual constructors for?”
Spoiler alert – there are no virtual constructors, and this rant’s not gonna be about tricky interview questions, nor about programming patterns.
We are going to talk about something I like to call good design instead ( and throw some numbers and code in the process ).
Enjoy!
Teasing and trolling
So just to make it clear, I think that the virtual destructor is one of the largest design flaws of the C++ language.
And when I say ‘a design flaw’, I mean something that always makes us double guess ourselves.
So here’s are 2 simple examples
// example 1
struct A
{
};
class B : public A
{
A* m_ptr; // I allocate memory somewhere in this class
};
// example 2
struct A
{
A* m_ptr; // I allocate memory somewhere in this class
};
class B : public A
{
};
Now – which of these should have a virtual destructor, which should have a regular destructor, and which one can go with a regular one?
Because the troll inside me wants to see you break, I’m gonna leave that question hanging for a while, and instead focus on how different programmers go about this:
A pragmatic programmer – this one will put the virtual destructors in each of these classes, just in case :/
A humble student – puts them where the allocations are made, then debugs the code dilligently to find out that depending on which type’s pointer you call delete, it doesn’t work quite the same. And after a few trials and errors decides to follow the pragmatic programmer’s footsteps.
A corpo programmer – doesn’t do a thing, let’s the QA find the bug for him – then he’ll fix it
I went through all those stages myself ( corpo programmer included ), and I learned one thing – a virtual destructor is an ambiguity.
Clarity
When I think of things that are well designed, I think of only two qualities:
It works – each time, every time
It’s immediately obvious how to use it, no second guessing
It works on my PC
Let’s start with the first one.
The virtual constructor, as well as the regular one, work – in every production grade compiler that has been released into public.
If you debug them, you’ll find out that they do exactly what they were designed to do.
But is that good enough?
Well, as the example above shows, when we change our perspective, suddenly “what it’s supposed to do” becomes less obvious.
Let’s do another example, shall we?
You created a class that wasn’t meant to be inherited from. But someone took it over after you, started extending it, refactoring it and ended up with a polymorphic hierarchy.
Now what he forgot about was to add the virtual keyword to the base class’s constructor.
How many of you have heard that story?
How many of you were the protagonists of that story?
Every single one? Really?! Well how ’bout that ;)
Square peg goes into the round hole
The other thing is being able to quickly tell what a thing does. I’m not saying that you don’t know how the destructor works.
What I’m saying is that you don’t know off hand all present and future contexts your code’s gonna be working under – there’s just no way unless it’s a Hello World application.
The sheer existence of two destructor variants makes you stop and think. And me for instance – I don’t want to spend time thinking about the language. I’d rather spend it thinking of all the cool things I could accomplish using it.
And if only the outcome of using the wrong one was insignificant.
Nooo – most often, following one of the Murphy’s Laws – it will lead to a huge memory leak and all sorts of nasty crashes.
Does that bode good design?
Socializing with the enemy
When I slammer something, I want to nail it good, so bear with me here.
C++ programmers used to think of them selves as the superior programmer race, inferior only to the god like assembly language programmers ( at least I did :P ).
Well no more. No matter how hard you try to stay pure at heart, at one point or another you’re gonna start using other languages.
Giving myself as an example, I do:
Python when I write plugins for Blender
C# when I script games in Unity3D
JavaScript because sometimes life gets you down ;)
And guess what – none of those, even though being object oriented, has a concept of a virtual destructor.
That gets me ( and I guess other people as well ) confused.
Then after spending a week coding something in Python, you suddenly jump back to C++ and mighty lord forbid that your first task that day is code refactoring.
So what’s it good for?
Well? Is it the performance?
We know that something bad happens with the performance when we start using inheritance.
That feeling of something going bad is usually associated with the size of the classes as well as the number of instructions it takes to call a method.
I compared a bunch of classes – their sizes and how they on the method calls.
Empty class size
The first was the size test that involved structures with no methods defined:
struct A
{
};
struct B
{
virtual ~B() {}
};
struct C : public A
{
};
struct D : public A
{
virtual ~D() {}
};
struct E : public B
{
};
struct F : public B
{
virtual ~F() {}
};
TEST( memTest, size )
{
CPPUNIT_ASSERT_EQUAL( 1, ( int )sizeof( A ) );
CPPUNIT_ASSERT_EQUAL( 4, ( int )sizeof( B ) );
CPPUNIT_ASSERT_EQUAL( 1, ( int )sizeof( C ) );
CPPUNIT_ASSERT_EQUAL( 4, ( int )sizeof( D ) );
CPPUNIT_ASSERT_EQUAL( 4, ( int )sizeof( E ) );
CPPUNIT_ASSERT_EQUAL( 4, ( int )sizeof( F ) );
}
Sizes of empty classes
What we can immediately see that the addition of the virtual destructor to either the class at hand (in case of B and D ), or the base class ( in case of E