Vtables C ++. Partie 2 (héritage virtuel + code généré par le compilateur)

La traduction de l'article a été préparée spécialement pour les étudiants du cours "Développeur C ++" . Est-il intéressant de se développer dans cette direction? Regardez l'enregistrement de la classe de pratique de Google Test Framework !



Partie 3 - Héritage virtuel


Dans les première et deuxième parties de cet article, nous avons parlé du fonctionnement des vtables dans les cas les plus simples, puis en héritage multiple. L'héritage virtuel complique encore plus la situation.


Comme vous vous en souvenez peut-être, l'héritage virtuel signifie que dans une classe particulière, il n'y a qu'une seule instance de la classe de base. Par exemple:


class ios ... class istream : virtual public ios ... class ostream : virtual public ios ... class iostream : public istream, public ostream 

Sans le mot-clé virtual ci-dessus, iostream fait deux instances d' ios qui pourraient causer des maux de tête pendant la synchronisation et seraient tout simplement inefficaces.


Pour comprendre l'héritage virtuel, nous considérerons le fragment de code suivant:


 #include <iostream> using namespace std; class Grandparent { public: virtual void grandparent_foo() {} int grandparent_data; }; class Parent1 : virtual public Grandparent { public: virtual void parent1_foo() {} int parent1_data; }; class Parent2 : virtual public Grandparent { public: virtual void parent2_foo() {} int parent2_data; }; class Child : public Parent1, public Parent2 { public: virtual void child_foo() {} int child_data; }; int main() { Child child; } 

Explorons l' child . Je commencerai par vider une grande quantité de mémoire exactement là où commence la vtable Child , comme nous l'avons fait dans les parties précédentes, puis j'analyserai les résultats. Je suggère de jeter un coup d'œil au résultat ici et d'y revenir lorsque je divulguerai les détails ci-dessous.


 (gdb) p child $1 = {<Parent1> = {<Grandparent> = {_vptr$Grandparent = 0x400998 <vtable for Child+96>, grandparent_data = 0}, _vptr$Parent1 = 0x400950 <vtable for Child+24>, parent1_data = 0}, <Parent2> = {_vptr$Parent2 = 0x400978 <vtable for Child+64>, parent2_data = 4195888}, child_data = 0} (gdb) x/600xb 0x400938 0x400938 <vtable for Child>: 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400940 <vtable for Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400948 <vtable for Child+16>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400950 <vtable for Child+24>: 0x70 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400958 <vtable for Child+32>: 0xa0 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400960 <vtable for Child+40>: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400968 <vtable for Child+48>: 0xf0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400970 <vtable for Child+56>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400978 <vtable for Child+64>: 0x90 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400980 <vtable for Child+72>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400988 <vtable for Child+80>: 0xe0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400990 <vtable for Child+88>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400998 <vtable for Child+96>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x4009a0 <VTT for Child>: 0x50 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009a8 <VTT for Child+8>: 0xf8 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009b0 <VTT for Child+16>: 0x18 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009b8 <VTT for Child+24>: 0x98 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009c0 <VTT for Child+32>: 0xb8 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009c8 <VTT for Child+40>: 0x98 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009d0 <VTT for Child+48>: 0x78 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009d8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009e0 <construction vtable for Parent1-in-Child>: 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009e8 <construction vtable for Parent1-in-Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009f0 <construction vtable for Parent1-in-Child+16>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009f8 <construction vtable for Parent1-in-Child+24>: 0x70 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400a00 <construction vtable for Parent1-in-Child+32>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a08 <construction vtable for Parent1-in-Child+40>: 0xe0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400a10 <construction vtable for Parent1-in-Child+48>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a18 <construction vtable for Parent1-in-Child+56>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400a20 <typeinfo name for Parent1>: 0x37 0x50 0x61 0x72 0x65 0x6e 0x74 0x31 0x400a28 <typeinfo name for Parent1+8>: 0x00 0x31 0x31 0x47 0x72 0x61 0x6e 0x64 0x400a30 <typeinfo name for Grandparent+7>: 0x70 0x61 0x72 0x65 0x6e 0x74 0x00 0x00 0x400a38 <typeinfo for Grandparent>: 0x50 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400a40 <typeinfo for Grandparent+8>: 0x29 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a48: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a50 <typeinfo for Parent1>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400a58 <typeinfo for Parent1+8>: 0x20 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a60 <typeinfo for Parent1+16>: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x400a68 <typeinfo for Parent1+24>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a70 <typeinfo for Parent1+32>: 0x03 0xe8 0xff 0xff 0xff 0xff 0xff 0xff 0x400a78: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a80 <construction vtable for Parent2-in-Child>: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a88 <construction vtable for Parent2-in-Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a90 <construction vtable for Parent2-in-Child+16>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a98 <construction vtable for Parent2-in-Child+24>: 0x90 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400aa0 <construction vtable for Parent2-in-Child+32>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400aa8 <construction vtable for Parent2-in-Child+40>: 0xf0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400ab0 <construction vtable for Parent2-in-Child+48>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400ab8 <construction vtable for Parent2-in-Child+56>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400ac0 <typeinfo name for Parent2>: 0x37 0x50 0x61 0x72 0x65 0x6e 0x74 0x32 0x400ac8 <typeinfo name for Parent2+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400ad0 <typeinfo for Parent2>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400ad8 <typeinfo for Parent2+8>: 0xc0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400ae0 <typeinfo for Parent2+16>: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x400ae8 <typeinfo for Parent2+24>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400af0 <typeinfo for Parent2+32>: 0x03 0xe8 0xff 0xff 0xff 0xff 0xff 0xff 0x400af8 <typeinfo name for Child>: 0x35 0x43 0x68 0x69 0x6c 0x64 0x00 0x00 0x400b00 <typeinfo for Child>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400b08 <typeinfo for Child+8>: 0xf8 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b10 <typeinfo for Child+16>: 0x02 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x400b18 <typeinfo for Child+24>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b20 <typeinfo for Child+32>: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b28 <typeinfo for Child+40>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b30 <typeinfo for Child+48>: 0x02 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x400b38 <vtable for Grandparent>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b40 <vtable for Grandparent+8>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b48 <vtable for Grandparent+16>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 

Wow, il y a beaucoup d'informations. Deux nouvelles questions surgissent tout de suite: qu'est-ce que le VTT et qu'est-ce que la construction vtable for X-in-Child ? Nous leur répondrons assez rapidement.
Commençons par la structure de la mémoire enfant:


La tailleValeur
8 octets_vptr $ Parent1
4 octetsparent1_data (+ 4 octets de remplissage)
8 octets_vptr $ Parent2
4 octetsparent2_data
4 octetschild_data
8 octets_vptr $ Grand-parent
4 octetsgrandparent_data (+ 4 octets de remplissage)

En effet, Child n'a qu'une seule instance de grand-parent. La chose non triviale est qu'il est le dernier en mémoire, bien qu'il soit le plus élevé de la hiérarchie.
Voici la structure de vtable :


L'adresseValeurTable des matières
0x4009380x20 (32)décalage de la base virtuelle (nous en discuterons bientôt)
0x4009400top_offset
0x4009480x400b00typeinfo pour enfant
0x4009500x400870Parent1 :: parent1_foo (). Le pointeur vtable Parent1 pointe ici.
0x4009580x4008a0Child :: child_foo ()
0x4009600x10 (16)décalage de la base virtuelle
0x400968-16top_offset
0x40090x400btypeinfo pour enfant
7000
0x4009780x400890Parent2 :: parent2_foo (). Le pointeur vtable Parent2 pointe ici.
0x4009800décalage de la base virtuelle
0x400988-32top_offset
0x4009900x400b00typeinfo pour enfant
0x4009980x400880Grand-parent :: grand-parent_foo (). Le pointeur vtable Grand-parent pointe ici.

Ci-dessus, il y a un nouveau concept - l' virtual-base offset . Bientôt, nous comprendrons ce qu'il fait là-bas.
Ensuite, explorons ces construction vtables aspect construction vtables . Voici la vtable for Parent1-in-Child construction vtable for Parent1-in-Child :


ValeurTable des matières
0x20 (32)décalage de la base virtuelle
0décalage supérieur
0x400a50typeinfo pour Parent1
0x400870Parent1 :: parent1_foo ()
0décalage de la base virtuelle
-32décalage supérieur
0x400a50typeinfo pour Parent1
0x400880Grand-parent :: grandparent_foo ()

Pour le moment, je pense qu'il serait plus compréhensible de décrire le processus que d'empiler plus de tableaux avec des nombres aléatoires sur vous. Donc:


Imaginez que vous êtes un Child . On vous demande de vous construire dans une nouvelle mémoire. Puisque vous Grandparent directement de Parent1 - Parent1 (ce que signifie l'héritage virtuel), vous appellerez directement son constructeur directement (s'il ne s'agissait pas d'un héritage virtuel, vous appelleriez le constructeur Parent1 , qui à son tour appellerait le constructeur de grand- Parent1 ). Vous définissez this += 32 octets, car c'est là que se trouvent les données des Grandparent et vous appelez le constructeur. Très simple.


Ensuite, il est temps de construire Parent1 . Parent1 peut supposer en toute sécurité qu'au moment où il se construit, Parent1 a déjà été créé, il peut donc, par exemple, accéder aux données et aux méthodes de grand-parent. Mais attendez, comment peut-il savoir où trouver ces données? Ils ne sont pas au même endroit avec les variables Parent1 !


La construction table for Parent1-in-Child entre en scène. Ce tableau sert à indiquer à Parent1 où trouver les éléments de données auxquels il peut accéder. this pointe vers les données de Parent1 . virtual-base offset indique où vous pouvez trouver les données des grands-parents: Étape 32 octets en avant à partir de cela et vous trouverez la mémoire des Grandparent . Comprenez-vous? le décalage de la base virtuelle est similaire à top_offset, mais pour les classes virtuelles.


Maintenant que nous comprenons cela, la construction de Parent2 est fondamentalement la même, en utilisant uniquement la construction table for Parent2-in-Child . En effet, Parent2-in-Child a un virtual-base offset de virtual-base offset de 16 octets.


Laissez les informations s'imprégner un peu. Êtes-vous prêt à continuer? Bon.
Revenons maintenant au VTT . Voici la structure VTT :


L'adresseValeurSymboleTable des matières
0x4009a00x400950vtable pour enfant + 24Entrées Parent1 dans vtable Child
0x4009a80x4009f8construction vtable pour Parent1-in-Child + 24Méthodes Parent1 dans Parent1-in-Child
0x4009b00x400a18table de construction pour Parent1-in-Child + 56Méthodes des grands-parents pour Parent1-in-Child
0x4009b80x400a98construction vtable pour Parent2-in-Child + 24Méthodes Parent2 dans Parent2-in-Child
0x4009c00x400ab8construction vtable pour Parent2-in-Child + 56`Méthodes de grand-parent pour Parent2-in-Child
0x4009c80x400998vtable pour enfant + 96`Entrées de grands-parents dans vtable Child
0x4009d00x400978vtable pour enfant + 64`Entrées Parent2 dans vtable Child

VTT signifie virtual-table table , ce qui signifie que c'est une table virtuelle. Il s'agit d'une table de traduction qui sait, par exemple, si le constructeur Parent1 pour un objet individuel, pour l'objet Parent1-in-Child ou pour Parent1-in-SomeOtherObject . Il apparaît toujours immédiatement après vtable , afin que le compilateur sache où le trouver. Par conséquent, il n'est pas nécessaire de stocker un autre pointeur dans les objets eux-mêmes.


Fuh ... beaucoup de détails, mais je pense que nous avons couvert tout ce que je voulais couvrir. Dans la quatrième partie, nous parlerons des détails des vtables niveau vtables . Ne sautez pas, car c'est probablement la partie la plus importante de cet article!


Partie 4 - Code généré par le compilateur


À ce stade de cet article, nous avons appris comment les typeinfo vtables et typeinfo s'intègrent dans nos binaires et comment le compilateur les utilise. Nous allons maintenant comprendre la partie du travail que le compilateur fait pour nous automatiquement.


Constructeurs


Pour le constructeur de n'importe quelle classe, le code suivant est généré:


  • Appel des constructions parent, le cas échéant;
  • Définir des pointeurs vtable, le cas échéant;
  • Initialisation des membres conformément à la liste des initialiseurs;
  • Exécution de code à l'intérieur des crochets du constructeur.

Tout ce qui précède peut se produire sans code explicite:


  • Les constructeurs parents démarrent automatiquement par défaut, sauf indication contraire;
  • Les membres sont initialisés par défaut s'ils n'ont pas de valeur ou d'entrées par défaut dans la liste d'initialisation;
  • Le constructeur entier peut être marqué = par défaut;
  • Seule l'affectation vtable est toujours masquée.

Voici un exemple:


 #include <iostream> #include <string> using namespace std; class Parent { public: Parent() { Foo(); } virtual ~Parent() = default; virtual void Foo() { cout << "Parent" << endl; } int i = 0; }; class Child : public Parent { public: Child() : j(1) { Foo(); } void Foo() override { cout << "Child" << endl; } int j; }; class Grandchild : public Child { public: Grandchild() { Foo(); s = "hello"; } void Foo() override { cout << "Grandchild" << endl; } string s; }; int main() { Grandchild g; } 

Écrivons un pseudo-code pour le constructeur de chaque classe:


ParentEnfantPetit-enfant
1. vtable = vtable Parent;1. Appelle le constructeur par défaut Parent;1. Appelle le constructeur par défaut Child;
2. i = 0;2. vtable = vtable Enfant;2. vtable = petit-enfant vtable;
3. Appelle Foo ();3. j = 1;3., Appelle le constructeur par défaut;
4. Appelle Foo ();4. Appelle Foo ();
5. Appelle l'opérateur = pour s;

Compte tenu de cela, il n'est pas surprenant que dans le contexte du constructeur de classe, vtable se réfère à la vtable de cette classe elle-même, et non à sa classe spécifique. Cela signifie que les appels virtuels sont résolus comme s'il n'y avait pas d'héritiers disponibles. Ainsi, la conclusion


 Parent Child Grandchild 

Qu'en est-il des fonctions virtuelles pures? S'ils ne sont pas implémentés (oui, vous pouvez implémenter des fonctions purement virtuelles, mais pourquoi en avez-vous besoin?), Vous irez probablement (et j'espère) directement à segfault. Certains compilateurs négligent l'erreur, ce qui est cool.


Destructeurs


Comme vous pouvez l'imaginer, les destructeurs se comportent de la même manière que les constructeurs, uniquement dans l'ordre inverse.


Voici un exercice rapide auquel réfléchir: pourquoi les destructeurs modifient-ils le pointeur vtable afin qu'il pointe vers sa propre classe, plutôt que de laisser un pointeur vers une classe spécifique? Réponse: puisque au moment du lancement du destructeur, toute classe héritière était déjà détruite. L'appel de méthodes de cette classe n'est pas ce que vous voulez faire.


Distribution implicite


Comme nous l'avons vu dans les deuxième et troisième parties , un pointeur sur un objet enfant n'est pas nécessairement égal au pointeur parent de la même instance (comme dans le cas de l'héritage multiple).


Cependant, pour vous (le développeur), il n'y a pas de travail supplémentaire pour appeler une fonction qui reçoit un pointeur parent. En effet, le compilateur modifie implicitement this lorsque vous ajoutez des pointeurs et des références aux classes parentes.


Distribution dynamique (RTTI)


Les typeinfo dynamiques utilisent des tables typeinfo , que nous avons examinées dans la première partie. Ils le font au moment de l'exécution, en regardant l'entrée typeinfo un pointeur avant vers quoi vtable pointeur vtable , et utilisent la classe à partir de là pour vérifier si la conversion est possible.


Cela explique le coût de dynamic_cast lorsqu'il est utilisé fréquemment.


Pointeurs de méthode


Je prévois d'écrire un article complet sur les pointeurs vers les méthodes à l'avenir. Avant cela, je voudrais souligner qu'un pointeur vers une méthode qui pointe vers une fonction virtuelle appellera en fait une méthode redéfinie (par opposition aux pointeurs vers des fonctions qui ne sont pas membres).


 // TODO:  ,     

Vérifiez-vous!


Vous pouvez maintenant vous expliquer pourquoi le fragment de code suivant se comporte comme il se comporte:


 #include <iostream> using namespace std; class FooInterface { public: virtual ~FooInterface() = default; virtual void Foo() = 0; }; class BarInterface { public: virtual ~BarInterface() = default; virtual void Bar() = 0; }; class Concrete : public FooInterface, public BarInterface { public: void Foo() override { cout << "Foo()" << endl; } void Bar() override { cout << "Bar()" << endl; } }; int main() { Concrete c; c.Foo(); c.Bar(); FooInterface* foo = &c; foo->Foo(); BarInterface* bar = (BarInterface*)(foo); bar->Bar(); //  "Foo()" - WTF? } 

Ceci conclut mon article en quatre parties. J'espère que vous apprenez quelque chose de nouveau, tout comme moi.

Source: https://habr.com/ru/post/fr480610/


All Articles