Notre ESN à taille humaine et 100 % locale est spécialisée sur 2 métiers : le développement et le pilotage autour de 4 expertises : le hardware, l’embarqué, le software et le web. 👨‍💻

Rejoindre ELEPHANT technologies, c’est travailler au sein d’une équipe dynamique et bienveillante. C’est aussi participer à une aventure humaine et valorisante qui permet à chaque collaborateur de grandir humainement et techniquement. 

 

Nous nous retrouvons aujourd’hui autour d’une thématique intéressante : les méthodes virtuelles dans les constructeurs et destructeurs en C ++. Notre elephantgénieuse, Lena, nous rappelle le fonctionnement et les points importants pour utiliser des méthodes virtuelles et nous donne quelques conseils pour éviter certaines erreurs

 

Alors, vous êtes prêts ?! Let’s go et bonne lecture 🐘 

 

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 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;

}


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();
 

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

            */

}

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:
 

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

}

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

 



💡 Conclusion 💡


Encore un grand merci à Lena pour la rédaction de cet article autour des méthodes virtuelles !

👉A ce sujet, vous retrouverez un article similaire en lien avec l’assemblage des comparaisons en C ++ si ça vous intéresse, juste ici : https://www.elephant-technologies.fr/actualite/9


 

Sources