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