Stop calling virtual methods in your constructor and destructor!
by Baduit
Why?
- It probably won’t do what you want
- It can crash
- Your code may give you a warning or even not compile 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”?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#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;
}
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?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#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;
}
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();
and during the execution, it crashes!
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#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
*/
}
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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
}
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.