Qu'est-ce qu'un vtable
?
Il peut être utile de savoir de quoi parle le message d'erreur avant d'essayer de le corriger. Je vais commencer à un niveau élevé, puis je descendrai jusqu'à plus de détails. De cette façon, les gens peuvent avancer une fois qu'ils sont à l'aise avec leur compréhension des vtables. … Et il y a un tas de gens qui sautent en ce moment. :) Pour ceux qui restent:
Une vtable est fondamentalement l'implémentation la plus courante du polymorphisme en C ++ . Lorsque vtables est utilisé, chaque classe polymorphe a une vtable quelque part dans le programme; vous pouvez le considérer comme un static
membre de données (caché) de la classe. Chaque objet d'une classe polymorphe est associé à la table virtuelle pour sa classe la plus dérivée. En vérifiant cette association, le programme peut travailler sa magie polymorphe. Mise en garde importante: une table virtuelle est un détail d'implémentation. Il n'est pas mandaté par la norme C ++, même si la plupart (tous?) Des compilateurs C ++ utilisent vtables pour implémenter un comportement polymorphe. Les détails que je présente sont des approches typiques ou raisonnables. Les compilateurs sont autorisés à s'en écarter!
Chaque objet polymorphe a un pointeur (caché) vers la table virtuelle pour la classe la plus dérivée de l'objet (éventuellement plusieurs pointeurs, dans les cas les plus complexes). En regardant le pointeur, le programme peut dire quel est le type "réel" d'un objet (sauf pendant la construction, mais sautons ce cas spécial). Par exemple, si un objet de type A
ne pointe pas vers la table de A
, cet objet est en fait un sous-objet de quelque chose dérivé deA
.
Le nom "vtable" vient de " v irtual function table ". Il s'agit d'un tableau qui stocke des pointeurs vers des fonctions (virtuelles). Un compilateur choisit sa convention pour la présentation de la table; une approche simple consiste à parcourir les fonctions virtuelles dans l'ordre dans lequel elles sont déclarées dans les définitions de classe. Lorsqu'une fonction virtuelle est appelée, le programme suit le pointeur de l'objet vers une table virtuelle, accède à l'entrée associée à la fonction souhaitée, puis utilise le pointeur de fonction stocké pour appeler la fonction correcte. Il y a plusieurs astuces pour faire fonctionner cela, mais je ne vais pas entrer dans celles-ci ici.
Où / quand est vtable
généré?
Une table virtuelle est générée automatiquement (parfois appelée "émise") par le compilateur. Un compilateur pourrait émettre une vtable dans chaque unité de traduction qui voit une définition de classe polymorphe, mais cela serait généralement inutile. Une alternative ( utilisée par gcc , et probablement par d'autres) consiste à choisir une seule unité de traduction dans laquelle placer la table virtuelle, de la même manière que vous choisiriez un seul fichier source dans lequel placer les membres de données statiques d'une classe. Si ce processus de sélection ne parvient pas à sélectionner des unités de traduction, la table virtuelle devient une référence non définie. D'où l'erreur, dont le message n'est certes pas particulièrement clair.
De même, si le processus de sélection choisit une unité de traduction, mais que ce fichier objet n'est pas fourni à l'éditeur de liens, alors la table virtuelle devient une référence non définie. Malheureusement, le message d'erreur peut être encore moins clair dans ce cas que dans le cas où le processus de sélection a échoué. (Merci aux répondeurs qui ont mentionné cette possibilité. Je l'aurais probablement oubliée autrement.)
Le processus de sélection utilisé par gcc prend tout son sens si nous partons de la tradition de consacrer un (seul) fichier source à chaque classe qui en a besoin pour sa mise en œuvre. Ce serait bien d'émettre la vtable lors de la compilation de ce fichier source. Appelons cela notre objectif. Cependant, le processus de sélection doit fonctionner même si cette tradition n'est pas respectée. Ainsi, au lieu de rechercher l'implémentation de la classe entière, recherchons l'implémentation d'un membre spécifique de la classe. Si la tradition est respectée - et si ce membre est en fait mis en œuvre - alors cela atteint l'objectif.
Le membre sélectionné par gcc (et potentiellement par d'autres compilateurs) est la première fonction virtuelle non en ligne qui n'est pas pure virtuelle. Si vous faites partie de la foule qui déclare les constructeurs et les destructeurs avant les autres fonctions membres, alors ce destructeur a de bonnes chances d'être sélectionné. (Vous vous souveniez de rendre le destructeur virtuel, non?) Il y a des exceptions; Je m'attends à ce que les exceptions les plus courantes soient lorsqu'une définition en ligne est fournie pour le destructeur et lorsque le destructeur par défaut est demandé (en utilisant " = default
").
L'astucieux pourrait remarquer qu'une classe polymorphe est autorisée à fournir des définitions en ligne pour toutes ses fonctions virtuelles. Cela ne fait-il pas échouer le processus de sélection? C'est le cas dans les anciens compilateurs. J'ai lu que les derniers compilateurs ont résolu cette situation, mais je ne connais pas les numéros de version pertinents. Je pourrais essayer de chercher cela, mais il est plus facile de coder autour ou d'attendre que le compilateur se plaint.
En résumé, il existe trois causes principales de l'erreur "référence non définie à vtable":
- Une fonction membre manque sa définition.
- Un fichier objet n'est pas lié.
- Toutes les fonctions virtuelles ont des définitions en ligne.
Ces causes sont en elles-mêmes insuffisantes pour provoquer l'erreur par elles-mêmes. Voici plutôt ce à quoi vous vous adresseriez pour résoudre l'erreur. Ne vous attendez pas à ce que la création intentionnelle d'une de ces situations produise définitivement cette erreur; il y a d'autres exigences. Attendez-vous à ce que la résolution de ces situations résout cette erreur.
(OK, le numéro 3 aurait peut-être suffi lorsque cette question a été posée.)
Comment corriger l'erreur?
Bienvenue aux gens qui sautent! :)
- Regardez votre définition de classe. Recherchez la première fonction virtuelle non en ligne qui n'est pas virtuelle pure (pas "
= 0
") et dont vous définissez la définition (pas " = default
").
- S'il n'y a pas une telle fonction, essayez de modifier votre classe pour qu'il y en ait une. (Erreur éventuellement résolue.)
- Voir également la réponse de Philip Thomas pour une mise en garde.
- Trouvez la définition de cette fonction. S'il manque, ajoutez-le! (Erreur éventuellement résolue.)
- Vérifiez votre commande de lien. S'il ne mentionne pas le fichier objet avec la définition de cette fonction, corrigez cela! (Erreur éventuellement résolue.)
- Répétez les étapes 2 et 3 pour chaque fonction virtuelle, puis pour chaque fonction non virtuelle, jusqu'à ce que l'erreur soit résolue. Si vous êtes toujours bloqué, répétez l'opération pour chaque membre de données statiques.
Exemple
Les détails de la procédure à suivre peuvent varier et parfois se diviser en questions distinctes (comme Qu'est-ce qu'une erreur de référence externe non définie / symbole non résolu et comment puis-je la corriger? ). Je vais cependant fournir un exemple de ce qu'il faut faire dans un cas spécifique qui pourrait dérouter les nouveaux programmeurs.
L'étape 1 mentionne la modification de votre classe afin qu'elle ait une fonction d'un certain type. Si la description de cette fonction vous dépasse, vous pourriez être dans la situation que j'ai l'intention de résoudre. Gardez à l'esprit que c'est une façon d'atteindre l'objectif; ce n'est pas le seul moyen, et il pourrait facilement y avoir de meilleurs moyens dans votre situation spécifique. Appelons votre classe A
. Votre destructeur est-il déclaré (dans votre définition de classe) comme
virtual ~A() = default;
ou
virtual ~A() {}
? Si c'est le cas, deux étapes changeront votre destructeur en le type de fonction que nous voulons. Tout d'abord, changez cette ligne en
virtual ~A();
Ensuite, mettez la ligne suivante dans un fichier source qui fait partie de votre projet (de préférence le fichier avec l'implémentation de classe, si vous en avez un):
A::~A() {}
Cela rend votre destructeur (virtuel) non en ligne et non généré par le compilateur. (N'hésitez pas à modifier les choses pour mieux correspondre à votre style de mise en forme de code, par exemple en ajoutant un commentaire d'en-tête à la définition de la fonction.)