ELEPHANT technologies, l’ESN locale et à taille humaine spécialisée sur 2 métiers : le développement et le pilotage autour de 4 expertises : hardware, embarqué, software et 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.
Let’s go !
👉 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!
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.
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 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.
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.
🐘 Encore un grand merci à Lena pour la rédaction de cet article autour des méthodes virtuelles !
Retrouvez un article similaire en lien avec l’assemblage des comparaisons en C ++ juste ici : https://www.elephant-technologies.fr/actualite/9
Sources :