Sponsored By

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.

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.

Piotr Trochim, Blogger

February 4, 2015

27 Min Read

Evil destructor

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:

  1. It works – each time, every time

  2. 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

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 and F ) increased the size of the class to 4 bytes.

Reason? Well, the infamous vtable of course (if I’m not mistaken).

Class with a single method size test

Next was the size test performed on classes with a single method.


struct A
{
    void f() {}
};

struct B
{
    virtual ~B() {}

    void f() {}
};

struct C
{
    ~C() {}

    virtual void f() {}
};

struct D
{
    virtual ~D() {}

    virtual void f() {}
};

TEST( memTest, size )
{
    CPPUNIT_ASSERT_EQUAL( 1, ( int )sizeof( A ) );
    CPPUNIT_ASSERT_EQUAL( 4, ( int )sizeof( B ) );
    CPPUNIT_ASSERT_EQUAL( 4, ( int )sizeof( C ) );
    CPPUNIT_ASSERT_EQUAL( 4, ( int )sizeof( D ) );
}

Classes with a single method

Classes with a single method

As you shrewdly observed, as soon as the word virtual came up, the size grew, telling us that a vtable had been introduced.

Well, we can pretty much tell by now what the results of the method call test are going to be, and we don’t even have to test it for the virtual destructor because we know that that word allows the destruction mechanism call the base class’s destructor, meaning that it will call multiple destruction methods anyway.

Mirror mirror…

I saved the best for last.
Remember my reference to that infamous interview question about the virtual contructor?

Well, we all know they don’t exist, because if you have an inheritance hierarchy, then the constructor from each class in the tree is guaranteed to get called ( it may be a different constructor for each one, depending on the parameters specified, but still ).

Let’s go back and revisit the theory about the destructors then.
What does the virtual keyword accomplish? Well exactly that how the constructors work out of the box – guarantees that all destructors in the inheritance tree get called.

To be even more specific, destructors work their way up the types hierarchy until they reach the class where the virtual destructor hasn’t been defined.
Moreover, the same rule apply as to the regular methods with the virtual keyword – once you defined a method virtual, every class inheriting from the class with that definition will mark that same method as virtual. That applies to the constructors as well.

So first – here’s an example illustrating the once virtual, always virtual principle.


struct A
{
    std::string& m_log;

    A( std::string& log ) : m_log( log ) {}

    virtual ~A()
    {
        m_log += "A";
    }
};

struct B : public A
{
    B( std::string& log ) : A( log ) {}

    ~B()
    {
        m_log += "B";
    }
};

struct C : public B
{
    C( std::string& log ) : B( log ) {}

    ~C()
    {
        m_log += "C";
    }
};

TEST( memTest, size )
{
    std::string log;
    A* a = new C( log );

    log = "";
    delete a;
    CPPUNIT_ASSERT_EQUAL( std::string("CBA"), log );
}

Notice that class’ B and C destructors are not virtual, but that doesn’t prevent the search from reaching the top of the hierarchy.
That’s because class’s A destructor is.


If you delete the virtual keyword from it though, this is what you’re going to get:

struct A
{

    std::string& m_log;

    A( std::string& log ) : m_log( log ) {}

    ~A()
    {
        m_log += "A";
    }
};

struct B : public A
{
    B( std::string& log ) : A( log ) {}

    ~B()
    {
        m_log += "B";
    }
};

TEST( memTest, size )
{
    std::string log;
    A* a = new B( log );

    log = "";
    delete a;
    CPPUNIT_ASSERT_EQUAL( std::string("A"), log );
}

But wait, it gets trickier than that.
What would happen if you introduced the virtual keyword to class B exclusively?

 

struct A
{
    std::string& m_log;

    A( std::string& log ) : m_log( log ) {}

    ~A()
    {
        m_log += "A";
    }
};

struct B : public A
{
    B( std::string& log ) : A( log ) {}

    virtual ~B()
    {
        m_log += "B";
    }
};

TEST( memTest, size )
{
    std::string log;
    A* a = new B( log );

    log = "";
    delete a; // CRASH !!!!!!!!!!!!
}

Yessir – a crash. Well, it actually depends on the compiler. I tested it on the good ‘ol MS Visual Studio 2013, but it is most likely going to crash on the majority of compilers.

Why?

Well – that single virtual keyword made the compiler introduce a vtable. But what’s the address of the A class’s destructor in it?
You probably have already guessed that.
If not – go run it in the debugger. I’m sure you’ll be pleased :)

POD types

Ok, we’re down to the only thing left that seems to justify the existence of virtual destructors. Unless you define them virtual, they keep the size of the class at bay.

Stands to reason – all the PODs that otherwise have the size constrained to the size of data they actually contain would otherwise be larger by the size of the vtable.

But wait, is that another keyword I see in the distance… __declspec(align(16)).

Let’s see what happens to our PODs which we so keenly align for various reasons, one of them being the performance :)


#define ALIGN_16 __declspec(align(16))

ALIGN_16 struct A
{
};

ALIGN_16 struct B
{
    int a;
};

ALIGN_16 struct C
{
    virtual ~C() {}
    int a;
};

TEST( memTest, size )
{
    CPPUNIT_ASSERT_EQUAL( 16, ( int )sizeof( A ) );
    CPPUNIT_ASSERT_EQUAL( 16, ( int )sizeof( B ) );
    CPPUNIT_ASSERT_EQUAL( 16, ( int )sizeof( C ) );
}

Aligned classes

Aligned classes

Whaaaat? Do my eyes deceive me? The last vestige of hope wiped out.

Conclusion

There you have it ladies and gentlemen.

This is why I think that the virtual destructor is a concept that never stood the test of time and not only exposes bad design of one of the core language features, but also introduces a lot of problems and gains us nothing at the same time.

I’m not trying to tell you what to do with it, just giving you insight needed to make an informed choice when it comes to a construct that comes every 5 minutes from under your fingertips.

I also do not claim that my point of view is the only sound one.
On the contrary – I would love to hear what you have to say about the subject, compare notes and share experiences.

So please – do elaborate.

Piotr Trochim, Feb 2015

---------------------

Reblogged from https://ptrochim.wordpress.com/2015/02/03/the-virtual-mess/

Read more about:

Featured Blogs
Daily news, dev blogs, and stories from Game Developer straight to your inbox

You May Also Like