DEV Community

Lena
Lena

Posted on

4 2

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

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

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
Lena
Comment hidden by post author
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

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay