Nous savons tous quelles sont les fonctions virtuelles en C ++, mais comment sont-elles implémentées à un niveau profond?
La vtable peut-elle être modifiée ou même directement accessible lors de l'exécution?
La vtable existe-t-elle pour toutes les classes, ou uniquement celles qui ont au moins une fonction virtuelle?
Les classes abstraites ont-elles simplement un NULL pour le pointeur de fonction d'au moins une entrée?
Avoir une seule fonction virtuelle ralentit-il toute la classe? Ou seulement l'appel à la fonction qui est virtuelle? Et la vitesse est-elle affectée si la fonction virtuelle est réellement écrasée ou non, ou est-ce que cela n'a aucun effet tant qu'elle est virtuelle.
c++
polymorphism
virtual-functions
vtable
Brian R. Bondy
la source
la source
Inside the C++ Object Model
parStanley B. Lippman
. (Section 4.2, page 124-131)Réponses:
Comment les fonctions virtuelles sont-elles implémentées à un niveau profond?
À partir de «Fonctions virtuelles en C ++» :
La vtable peut-elle être modifiée ou même directement accessible lors de l'exécution?
Universellement, je crois que la réponse est «non». Vous pouvez faire un peu de mémoire pour trouver la vtable, mais vous ne savez toujours pas à quoi ressemble la signature de la fonction pour l'appeler. Tout ce que vous voudriez réaliser avec cette capacité (que le langage prend en charge) devrait être possible sans accéder directement à la vtable ou sans la modifier au moment de l'exécution. Notez également que la spécification du langage C ++ ne spécifie que les vtables sont nécessaires - cependant, c'est ainsi que la plupart des compilateurs implémentent des fonctions virtuelles.
La vtable existe-t-elle pour tous les objets ou uniquement pour ceux qui ont au moins une fonction virtuelle?
Je crois la réponse ici est "cela dépend de l'implémentation" puisque la spécification ne nécessite pas de vtables en premier lieu. Cependant, dans la pratique, je crois que tous les compilateurs modernes ne créent une vtable que si une classe a au moins 1 fonction virtuelle. Il y a une surcharge d'espace associée à la vtable et une surcharge de temps associée à l'appel d'une fonction virtuelle par rapport à une fonction non virtuelle.
Les classes abstraites ont-elles simplement un NULL pour le pointeur de fonction d'au moins une entrée?
La réponse est qu'il n'est pas spécifié par la spécification du langage, donc cela dépend de l'implémentation. L'appel de la fonction virtuelle pure entraîne un comportement indéfini si elle n'est pas définie (ce qui n'est généralement pas le cas) (ISO / CEI 14882: 2003 10.4-2). En pratique, il alloue un emplacement dans la vtable pour la fonction mais ne lui attribue pas d'adresse. Cela laisse la vtable incomplète, ce qui nécessite que les classes dérivées implémentent la fonction et complètent la vtable. Certaines implémentations placent simplement un pointeur NULL dans l'entrée vtable; d'autres implémentations placent un pointeur vers une méthode factice qui fait quelque chose de similaire à une assertion.
Notez qu'une classe abstraite peut définir une implémentation pour une fonction virtuelle pure, mais que cette fonction ne peut être appelée qu'avec une syntaxe d'ID qualifié (c'est-à-dire en spécifiant complètement la classe dans le nom de la méthode, comme pour appeler une méthode de classe de base à partir d'un Classe dérivée). Ceci est fait pour fournir une implémentation par défaut facile à utiliser, tout en exigeant toujours qu'une classe dérivée fournisse un remplacement.
Le fait d'avoir une seule fonction virtuelle ralentit-il toute la classe ou seulement l'appel à la fonction qui est virtuelle?
Cela touche à mes connaissances, alors s'il vous plaît, aidez-moi ici si je me trompe!
Je crois que seules les fonctions virtuelles de la classe subissent les performances temporelles liées à l'appel d'une fonction virtuelle par rapport à une fonction non virtuelle. La surcharge d'espace pour la classe est là de toute façon. Notez que s'il y a une vtable, il n'y en a qu'une par classe , pas une par objet .
La vitesse est-elle affectée si la fonction virtuelle est réellement remplacée ou non, ou est-ce que cela n'a aucun effet tant qu'elle est virtuelle?
Je ne crois pas que le temps d'exécution d'une fonction virtuelle qui est remplacée diminue par rapport à l'appel de la fonction virtuelle de base. Cependant, il y a une surcharge d'espace supplémentaire pour la classe associée à la définition d'une autre vtable pour la classe dérivée par rapport à la classe de base.
Ressources supplémentaires:
http://www.codersource.net/published/view/325/virtual_functions_in.aspx (via une machine de retour)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/ cxx-abi / abi.html # vtable
la source
Pas de manière portable, mais si vous ne craignez pas les sales tours, bien sûr!
Dans la plupart des compilateurs que j'ai vus, le vtbl * est les 4 premiers octets de l'objet, et le contenu de la vtbl est simplement un tableau de pointeurs membres (généralement dans l'ordre dans lequel ils ont été déclarés, avec la classe de base en premier). Il existe bien sûr d'autres mises en page possibles, mais c'est ce que j'ai généralement observé.
Maintenant, pour tirer quelques manigances ...
Changement de classe au moment de l'exécution:
Remplacement d'une méthode pour toutes les instances (monkeypatching une classe)
Celui-ci est un peu plus délicat, car le vtbl lui-même est probablement en mémoire morte.
Ce dernier est plutôt susceptible de faire des vérificateurs de virus et le lien se réveiller et en prendre note, en raison des manipulations de mprotect. Dans un processus utilisant le bit NX, cela peut échouer.
la source
Avoir une seule fonction virtuelle ralentit-il toute la classe?
Avoir des fonctions virtuelles ralentit toute la classe dans la mesure où un élément de données supplémentaire doit être initialisé, copié,… lorsqu'il s'agit d'un objet d'une telle classe. Pour une classe comptant une demi-douzaine de membres environ, la différence devrait être négligeable. Pour une classe qui ne contient qu'un seul
char
membre, ou aucun membre du tout, la différence peut être notable.En dehors de cela, il est important de noter que tous les appels à une fonction virtuelle ne sont pas des appels de fonction virtuelle. Si vous avez un objet d'un type connu, le compilateur peut émettre du code pour un appel de fonction normal, et peut même incorporer ladite fonction s'il en a envie. Ce n'est que lorsque vous effectuez des appels polymorphes, via un pointeur ou une référence qui pourrait pointer vers un objet de la classe de base ou vers un objet d'une classe dérivée, que vous avez besoin de l'indirection vtable et que vous la payez en termes de performances.
Les étapes que le matériel doit suivre sont essentiellement les mêmes, que la fonction soit écrasée ou non. L'adresse de la vtable est lue à partir de l'objet, le pointeur de fonction extrait de l'emplacement approprié et la fonction appelée par le pointeur. En termes de performances réelles, les prédictions de branche peuvent avoir un certain impact. Ainsi, par exemple, si la plupart de vos objets font référence à la même implémentation d'une fonction virtuelle donnée, il y a une certaine chance que le prédicteur de branche prédise correctement la fonction à appeler avant même que le pointeur n'ait été récupéré. Mais peu importe la fonction qui est la plus courante: il peut s'agir de la plupart des objets déléguant au cas de base non écrasé, ou de la plupart des objets appartenant à la même sous-classe et donc déléguant au même cas écrasé.
comment sont-ils mis en œuvre à un niveau profond?
J'aime l'idée de jheriko pour démontrer cela en utilisant une implémentation fictive. Mais j'utiliserais C pour implémenter quelque chose qui ressemble au code ci-dessus, afin que le niveau bas soit plus facilement visible.
classe parent Foo
classe dérivée Bar
fonction f exécution d'un appel de fonction virtuelle
Vous pouvez donc voir qu'une vtable n'est qu'un bloc statique en mémoire, contenant principalement des pointeurs de fonction. Chaque objet d'une classe polymorphe pointera vers la vtable correspondant à son type dynamique. Cela rend également plus claire la connexion entre RTTI et les fonctions virtuelles: vous pouvez vérifier le type d'une classe simplement en regardant vers quelle vtable elle pointe. Ce qui précède est simplifié de plusieurs manières, comme par exemple l'héritage multiple, mais le concept général est solide.
Si
arg
est de typeFoo*
et que vous prenezarg->vtable
, mais est en fait un objet de typeBar
, vous obtenez toujours l'adresse correcte duvtable
. En effet, levtable
est toujours le premier élément à l'adresse de l'objet, qu'il soit appelévtable
oubase.vtable
dans une expression correctement typée.la source
Habituellement, avec un VTable, un tableau de pointeurs vers des fonctions.
la source
Cette réponse a été intégrée à la réponse du wiki communautaire
La réponse à cela est qu'elle n'est pas spécifiée - l'appel de la fonction virtuelle pure entraîne un comportement indéfini si elle n'est pas définie (ce qui n'est généralement pas le cas) (ISO / CEI 14882: 2003 10.4-2). Certaines implémentations placent simplement un pointeur NULL dans l'entrée vtable; d'autres implémentations placent un pointeur vers une méthode factice qui fait quelque chose de similaire à une assertion.
Notez qu'une classe abstraite peut définir une implémentation pour une fonction virtuelle pure, mais que cette fonction ne peut être appelée qu'avec une syntaxe d'ID qualifié (c'est-à-dire en spécifiant complètement la classe dans le nom de la méthode, comme pour appeler une méthode de classe de base à partir d'un Classe dérivée). Ceci est fait pour fournir une implémentation par défaut facile à utiliser, tout en exigeant toujours qu'une classe dérivée fournisse un remplacement.
la source
Vous pouvez recréer la fonctionnalité des fonctions virtuelles en C ++ en utilisant des pointeurs de fonction en tant que membres d'une classe et des fonctions statiques en tant qu'implémentations, ou en utilisant un pointeur vers des fonctions membres et des fonctions membres pour les implémentations. Il n'y a que des avantages de notation entre les deux méthodes ... en fait, les appels de fonction virtuelle ne sont en eux-mêmes qu'une commodité de notation. En fait, l'héritage n'est qu'une commodité de notation ... tout peut être implémenté sans utiliser les fonctionnalités du langage pour l'héritage. :)
Ce qui suit est de la merde non testé, probablement du code bogué, mais j'espère démontre l'idée.
par exemple
la source
void(*)(Foo*) MyFunc;
est-ce une syntaxe Java?Je vais essayer de faire simple :)
Nous savons tous quelles sont les fonctions virtuelles en C ++, mais comment sont-elles implémentées à un niveau profond?
Il s'agit d'un tableau avec des pointeurs vers des fonctions, qui sont des implémentations d'une fonction virtuelle particulière. Un index dans ce tableau représente un index particulier d'une fonction virtuelle définie pour une classe. Cela inclut les fonctions virtuelles pures.
Lorsqu'une classe polymorphe dérive d'une autre classe polymorphe, nous pouvons avoir les situations suivantes:
La vtable peut-elle être modifiée ou même directement accessible lors de l'exécution?
Pas de manière standard - il n'y a pas d'API pour y accéder. Les compilateurs peuvent avoir des extensions ou des API privées pour y accéder, mais ce n'est peut-être qu'une extension.
La vtable existe-t-elle pour toutes les classes, ou uniquement celles qui ont au moins une fonction virtuelle?
Seuls ceux qui ont au moins une fonction virtuelle (même destructrice) ou dérivent au moins une classe qui a sa vtable ("est polymorphe").
Les classes abstraites ont-elles simplement un NULL pour le pointeur de fonction d'au moins une entrée?
C'est une implémentation possible, mais plutôt non pratiquée. Au lieu de cela, il y a généralement une fonction qui imprime quelque chose comme "fonction virtuelle pure appelée" et le fait
abort()
. L'appel à cela peut se produire si vous essayez d'appeler la méthode abstraite dans le constructeur ou le destructeur.Avoir une seule fonction virtuelle ralentit-il toute la classe? Ou seulement l'appel à la fonction qui est virtuelle? Et la vitesse est-elle affectée si la fonction virtuelle est réellement écrasée ou non, ou est-ce que cela n'a aucun effet tant qu'elle est virtuelle.
Le ralentissement dépend uniquement du fait que l'appel est résolu en appel direct ou en appel virtuel. Et rien d'autre n'a d'importance. :)
Si vous appelez une fonction virtuelle via un pointeur ou une référence à un objet, elle sera toujours implémentée en tant qu'appel virtuel - car le compilateur ne peut jamais savoir quel type d'objet sera affecté à ce pointeur au moment de l'exécution, et s'il s'agit d'un classe dans laquelle cette méthode est remplacée ou non. Seulement dans deux cas, le compilateur peut résoudre l'appel à une fonction virtuelle comme un appel direct:
final
dans la classe vers laquelle vous avez un pointeur ou une référence à travers laquelle vous l'appelez ( uniquement en C ++ 11 ). Dans ce cas, le compilateur sait que cette méthode ne peut subir aucune autre substitution et qu'elle ne peut être que la méthode de cette classe.Notez cependant que les appels virtuels n'ont qu'une surcharge de déréférencement de deux pointeurs. L'utilisation de RTTI (bien que disponible uniquement pour les classes polymorphes) est plus lente que l'appel de méthodes virtuelles, si vous trouvez un cas pour implémenter la même chose de deux manières. Par exemple, définir
virtual bool HasHoof() { return false; }
, puis remplacer uniquement commebool Horse::HasHoof() { return true; }
cela vous donnerait la possibilité d'appelerif (anim->HasHoof())
qui sera plus rapide que d'essayerif(dynamic_cast<Horse*>(anim))
. En effet,dynamic_cast
il faut parcourir la hiérarchie des classes dans certains cas, même de manière récursive pour voir s'il peut être construit le chemin à partir du type de pointeur réel et du type de classe souhaité. Alors que l'appel virtuel est toujours le même - déréférencer deux pointeurs.la source
Voici une implémentation manuelle exécutable de la table virtuelle dans le C ++ moderne. Il a une sémantique bien définie, pas de hacks et pas de
void*
.Remarque:
.*
et->*
sont des opérateurs différents de*
et->
. Les pointeurs de fonction membre fonctionnent différemment.la source
Chaque objet a un pointeur vtable qui pointe vers un tableau de fonctions membres.
la source
Quelque chose qui n'est pas mentionné ici dans toutes ces réponses, c'est qu'en cas d'héritage multiple, où les classes de base ont toutes des méthodes virtuelles. La classe héritière a plusieurs pointeurs vers un vmt. Le résultat est que la taille de chaque instance d'un tel objet est plus grande. Tout le monde sait qu'une classe avec des méthodes virtuelles a 4 octets supplémentaires pour le vmt, mais en cas d'héritage multiple, c'est pour chaque classe de base qui a des méthodes virtuelles multipliées par 4. 4 étant la taille du pointeur.
la source
Les réponses de Burly sont correctes ici, sauf pour la question:
Les classes abstraites ont-elles simplement un NULL pour le pointeur de fonction d'au moins une entrée?
La réponse est qu'aucune table virtuelle n'est créée du tout pour les classes abstraites. Cela n'est pas nécessaire car aucun objet de ces classes ne peut être créé!
En d'autres termes, si nous avons:
Le pointeur vtbl accédé via pB sera le vtbl de la classe D. C'est exactement comment le polymorphisme est implémenté. Autrement dit, comment les méthodes D sont accessibles via pB. Il n'y a pas besoin d'un vtbl pour la classe B.
En réponse au commentaire de Mike ci-dessous ...
Si la classe B dans ma description a une méthode virtuelle foo () qui n'est pas remplacée par D et une méthode virtuelle bar () qui est remplacée, alors le vtbl de D aura un pointeur vers B's foo () et vers sa propre barre () . Il n'y a toujours pas de vtbl créé pour B.
la source
B
devrait être nécessaire. Ce n'est pas parce que certaines de ses méthodes ont des implémentations (par défaut) qu'elles doivent être stockées dans une vtable. Mais j'ai juste exécuté votre code (modulo quelques correctifs pour le faire compiler)gcc -S
suivi dec++filt
et il y a clairement une vtable pour y êtreB
incluse. Je suppose que cela pourrait être dû au fait que la vtable stocke également des données RTTI telles que les noms de classe et l'héritage. Il peut être nécessaire pour un fichierdynamic_cast<B*>
. Même-fno-rtti
ne fait pas disparaître la vtable. Avecclang -O3
au lieu degcc
ça, il a soudainement disparu.une preuve de concept très mignonne que j'ai faite un peu plus tôt (pour voir si l'ordre d'héritage compte); faites-moi savoir si votre implémentation de C ++ le rejette réellement (ma version de gcc ne donne qu'un avertissement pour l'attribution de structures anonymes, mais c'est un bogue), je suis curieux.
CCPolite.h :
CCPolite_constructor.h :
main.c :
production:
notez que comme je n'attribue jamais mon faux objet, il n'y a pas besoin de faire de destruction; les destructeurs sont automatiquement placés à la fin de la portée des objets alloués dynamiquement pour récupérer la mémoire de l'objet littéral lui-même et du pointeur vtable.
la source