DEV Community

Lena
Lena

Posted on

Stop calling virtual methods in your constructor and destructor!

Why?

  • It probably won't do what you want
  • It can crash
  • Your code may give you a warning or even not compile or link at all and that's how a good compiler should act because you should not do that
  • Even if it does what you want, it is error prone because there is a high risk someone else might miss it or won’t understand it

Let's look at this code, what do you think is printed on the console, "Base" or "Derived"?

#include <iostream>

class Base
{
    public:
        Base()
        {
            speak();
        }

        virtual void speak() const
        {
            std::cout << "Base" << std::endl;
        }
};

class Derived : public Base
{
    public:
        virtual void speak() const override
        {
            std::cout << "Derived" << std::endl;
        }
};

int main()
{
    Derived d;
}
Enter fullscreen mode Exit fullscreen mode

It prints "Base" and not "Derived" as you might think. Despite seeming counter intuitive and weird, it is totally normal.

You can see it on compiler explorer here.

Now let's take this code, what does it do?

#include <iostream>

class Base
{
    public:
        Base()
        {
            speak();
        }

        virtual void speak() const = 0;
};

class Derived : public Base
{
    public:
        virtual void speak() const override
        {
            std::cout << "Derived" << std::endl;
        }
};

int main()
{
    Derived d;
}
Enter fullscreen mode Exit fullscreen mode

Compiler explorer link.

There is a warning during the compilation looking like this with gcc:

    <source>: In constructor 'Base::Base()':
    <source>:7:14: warning: pure virtual 'virtual void Base::speak() const' called from constructor
        7 |         speak();
Enter fullscreen mode Exit fullscreen mode

and it does not link!

Explanations

How an object of a class using inheritance is constructed and destroyed?

Like an onion, an object has layers. Let’s take the following code as example:

#include <iostream>

class GrandParent
{
    public:
        GrandParent()
        {
            std::cout << "GrandParent" << std::endl;
        }

        ~GrandParent()
        {
            std::cout << "~GrandParent" << std::endl;
        }
};

class Parent : public GrandParent
{
    public:
        Parent()
        {
            std::cout << "Parent" << std::endl;
        }

        ~Parent()
        {
            std::cout << "~Parent" << std::endl;
        }
};

class Child : public Parent
{
    public:
        Child()
        {
            std::cout << "Child" << std::endl;
        }

        ~Child()
        {
            std::cout << "~Child" << std::endl;
        }
};

int main()
{
    Child onion;
    /* output :
        - GrandParent
        - Parent
        - Child
        - ~Child
        - ~Parent
        - ~GrandParent
    */
}
Enter fullscreen mode Exit fullscreen mode

Compiler explorer link

The deepest layer corresponds to the class GrandParent: it is constructed first and destroyed last.

The middle layer corresponds to the class Parent: it is constructed and destroyed second.

Lastly, the external layer is corresponding to the class Child and is constructed last and destroyed first.

A little reminder about how virtual methods works

Each class with at least one virtual method will have a vtable, it holds the pointers to all the virtual methods of the class. When an object is created, it will have a pointer to this vtable so it can access when needed. Here's a little example to illustrate the idea:

#include <iostream>

struct Virtual
{
    virtual void vmethod() {}
};

struct NoVirtual {};

int main()
{
    std::cout << sizeof(Virtual) << std::endl; // 8
    static_assert(sizeof(Virtual) == sizeof(void*)); // Same size as a pointer

    std::cout << sizeof(NoVirtual) << std::endl; // 1, this is the smallest size an object can have and still have a unique address
}
Enter fullscreen mode Exit fullscreen mode

Compiler explorer link

You can see that the size of an object with a virtual method is a bit bigger, because of the pointer to the vtable.

The clash

The object has a pointer to the vtable, that’s nice, but this pointer needs to bet set. Remember the metaphor about the onion? Well, the pointer is set during the very beginning of the creation of each layer. You should now grasp the problem: Take for example a class Base inherited by a class Derived. If you try calling a virtual method in the constructor of Base, when you create an object of type Derived, it begins the construction of the “Base” layer by setting the pointer to the vtable, then it calls Derived own constructor. In your constructor you have the call to the virtual method, the vtable you have access to right now is the vtable to Base instead of Derived.

The same thing happens in opposite order during the destructor call.

Even if it seems counter intuitive, as we have seen, this behavior is logic.

What if I know what I do?

Even if you think you know what you are doing, don’t do it. It will easily backfire; the code is still confusing. A future maintainer might get confused and make a mistake because even if they have the knowledge about how virtual call works during constructor and destructor, they can miss it as it is hard to spot.

Sources

Top comments (7)

Collapse
 
pgradot profile image
Pierre Gradot

Note : il y a un problème d'affichage des liens dans ton article

Collapse
 
baduit profile image
Lena

Effectivement, j'avais pas vu que ça gérait pas cette syntaxe pour les liens, c'est corrigé

Collapse
 
pauljlucas profile image
Paul J. Lucas

Nit: In the C++ world, "methods" are referred to as "member functions."

Collapse
 
baduit profile image
Info Comment hidden by post author - thread only accessible via permalink
Lena

I perfectly know that, there is no need to be rude. When writing an article you need know what is your target audience. In my case for this article this is not targetted at beginner/intermediate level in C++, most of them won't probably exactly the term used in the standard but will have knowledgde about other langage where the term "methods" or from their OO courses.
Maybe I should add a paragraph explaining that, but while writing it I made the choice of not mentionning it, maybe it is was a bad choice, but criticism does not allow you to be rude.
I really hope I misunderstood the meaning of "nit" in this context.

Collapse
 
pauljlucas profile image
Paul J. Lucas

"Nit" = "Minor point, almost not worth mentioning." Nothing I wrote would be interpreted by a native English speaker as rude. Perhaps you shouldn't assume someone is being rude to start.

Personally, I always try to refer to things by the proper names. If you are teaching C++, best to use the correct C++ terms regardless of what they're called in other languages. To clarify things for non-C++ readers, I would have written as expository note:

What are called "methods" in other object-oriented programming languages are called "member functions" in C++.

And then used "member functions" thereafter.

Thread Thread
 
baduit profile image
Lena

Oh sorry, the meaning I got from my online dictionary was something like "moron" or "cretin". I guess I overreacted, sorry again 😅
You got a point, I could have introduced it this way. I will think about update the article.

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

Yes, "nit" is commonly short for "nitpick" — though I now learned it can also be used as short for "nitwit," but that's not how I meant it.

Some comments have been hidden by the post's author - find out more