Comment les fonctions virtuelles et vtable sont-elles implémentées?

109

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.

Brian R. Bondy
la source
2
Suggérer de lire le chef-d'œuvre Inside the C++ Object Modelpar Stanley B. Lippman. (Section 4.2, page 124-131)
smwikipedia

Réponses:

123

Comment les fonctions virtuelles sont-elles implémentées à un niveau profond?

À partir de «Fonctions virtuelles en C ++» :

Chaque fois qu'un programme a une fonction virtuelle déclarée, une table av est construite pour la classe. La v-table se compose des adresses des fonctions virtuelles pour les classes qui contiennent une ou plusieurs fonctions virtuelles. L'objet de la classe contenant la fonction virtuelle contient un pointeur virtuel qui pointe vers l'adresse de base de la table virtuelle en mémoire. Chaque fois qu'il y a un appel de fonction virtuelle, la v-table est utilisée pour résoudre l'adresse de la fonction. Un objet de la classe qui contient une ou plusieurs fonctions virtuelles contient un pointeur virtuel appelé vptr au tout début de l'objet dans la mémoire. Par conséquent, la taille de l'objet dans ce cas augmente de la taille du pointeur. Ce vptr contient l'adresse de base de la table virtuelle en mémoire. Notez que les tables virtuelles sont spécifiques à la classe, c'est-à-dire il n'y a qu'une seule table virtuelle pour une classe quel que soit le nombre de fonctions virtuelles qu'elle contient. Cette table virtuelle contient à son tour les adresses de base d'une ou plusieurs fonctions virtuelles de la classe. Au moment où une fonction virtuelle est appelée sur un objet, le vptr de cet objet fournit l'adresse de base de la table virtuelle pour cette classe en mémoire. Cette table est utilisée pour résoudre l'appel de fonction car elle contient les adresses de toutes les fonctions virtuelles de cette classe. Voici comment la liaison dynamique est résolue lors d'un appel de fonction virtuelle. le vptr de cet objet fournit l'adresse de base de la table virtuelle pour cette classe en mémoire. Cette table est utilisée pour résoudre l'appel de fonction car elle contient les adresses de toutes les fonctions virtuelles de cette classe. Voici comment la liaison dynamique est résolue lors d'un appel de fonction virtuelle. le vptr de cet objet fournit l'adresse de base de la table virtuelle pour cette classe en mémoire. Cette table est utilisée pour résoudre l'appel de fonction car elle contient les adresses de toutes les fonctions virtuelles de cette classe. Voici comment la liaison dynamique est résolue lors d'un appel de fonction virtuelle.

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

Zach Burlingame
la source
2
Ce ne serait pas conforme à la philosophie de Stroustrup de C ++ pour un compilateur de mettre un pointeur vtable inutile dans un objet qui n'en a pas besoin. La règle est que vous n'obtenez pas de surcharge qui n'est pas en C à moins que vous ne le demandiez, et il est impoli pour les compilateurs de casser cela.
Steve Jessop
3
Je suis d'accord qu'il serait insensé pour tout compilateur qui se prend au sérieux d'utiliser une vtable alors qu'aucune fonction virtuelle n'existe. Cependant, j'ai senti qu'il était important de souligner qu'à ma connaissance, le standard C ++ ne le / requiert pas, donc soyez prévenu avant d'en dépendre.
Zach Burlingame
8
Même les fonctions virtuelles peuvent être appelées de manière non virtuelle. C'est en fait assez courant: si l'objet est sur la pile, dans la portée, le compilateur connaîtra le type exact et optimisera la recherche de vtable. Cela est particulièrement vrai pour le dtor, qui doit être appelé dans la même portée de pile.
MSalters le
1
Je crois que lorsqu'une classe a au moins une fonction virtuelle, chaque objet a une vtable, et non une pour la classe entière.
Asaf R
3
Implémentation commune: chaque objet a un pointeur vers une vtable; la classe possède la table. La magie de construction consiste simplement à mettre à jour le pointeur vtable dans le ctor dérivé, une fois le ctor de base terminé.
MSalters le
31
  • La vtable peut-elle être modifiée ou même directement accessible lors de l'exécution?

Pas de manière portable, mais si vous ne craignez pas les sales tours, bien sûr!

AVERTISSEMENT : cette technique n'est pas recommandée pour les enfants, les adultes de moins de 969 ans ou les petites créatures à fourrure d'Alpha Centauri. Les effets secondaires peuvent inclure des démons qui volent hors de votre nez , l'apparition brusque de Yog-Sothoth tant qu'approbateur requis lors de toutes les révisions de code ultérieures, ou l'ajout rétroactif de IHuman::PlayPiano()à toutes les instances existantes]

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

class A {
  public:
  virtual int f1() = 0;
};
class B : public A {
  public:
  virtual int f1() { return 1; }
  virtual int f2() { return 2; }
};
class C : public A {
  public:
  virtual int f1() { return -1; }
  virtual int f2() { return -2; }
};

A *x = new B;
A *y = new C;
A *z = new C;

Maintenant, pour tirer quelques manigances ...

Changement de classe au moment de l'exécution:

std::swap(*(void **)x, *(void **)y);
// Now x is a C, and y is a B! Hope they used the same layout of members!

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.

int f3(A*) { return 0; }

mprotect(*(void **)x,8,PROT_READ|PROT_WRITE|PROT_EXEC);
// Or VirtualProtect on win32; this part's very OS-specific
(*(int (***)(A *)x)[0] = f3;
// Now C::f1() returns 0 (remember we made x into a C above)
// so x->f1() and z->f1() both return 0

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.

puetzk
la source
6
Hmm. Il est inquiétant que cela ait reçu une prime. J'espère que cela ne signifie pas que @Mobilewits pense que de telles manigances sont en fait une bonne idée ...
puetzk
1
Veuillez envisager de décourager l'utilisation de cette technique, clairement et fortement, plutôt que de «faire un clin d'œil».
einpoklum
" Le contenu de la vtbl est simplement un tableau de pointeurs de membre " en fait c'est un enregistrement (une structure) avec des entrées différentes, qui sont espacées de manière uniforme
curiousguy
1
Vous pouvez le regarder de toute façon; les pointeurs de fonction ont des signatures différentes, et donc différents types de pointeurs; en ce sens, c'est en effet une structure. Mais dans d'autres contextes, mais l'idée d'index vtbl est utile (par exemple, ActiveX l'utilise comme il décrit les doubles interfaces dans les typelibs), qui est une vue plus sous forme de tableau.
puetzk
17

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.

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 charmembre, 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.

struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
  Foo x; x.a(); // non-virtual: always calls Foo::a()
  Bar y; y.a(); // non-virtual: always calls Bar::a()
  arg.a();      // virtual: must dispatch via vtable
  Foo z = arg;  // copy constructor Foo::Foo(const Foo&) will convert to Foo
  z.a();        // non-virtual Foo::a, since z is a Foo, even if arg was not
}

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

typedef struct Foo_t Foo;   // forward declaration
struct slotsFoo {           // list all virtual functions of Foo
  const void *parentVtable; // (single) inheritance
  void (*destructor)(Foo*); // virtual destructor Foo::~Foo
  int (*a)(Foo*);           // virtual function Foo::a
};
struct Foo_t {                      // class Foo
  const struct slotsFoo* vtable;    // each instance points to vtable
};
void destructFoo(Foo* self) { }     // Foo::~Foo
int aFoo(Foo* self) { return 1; }   // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
  0,                                // no parent class
  destructFoo,
  aFoo
};
void constructFoo(Foo* self) {      // Foo::Foo()
  self->vtable = &vtableFoo;        // object points to class vtable
}
void copyConstructFoo(Foo* self,
                      Foo* other) { // Foo::Foo(const Foo&)
  self->vtable = &vtableFoo;        // don't copy from other!
}

classe dérivée Bar

typedef struct Bar_t {              // class Bar
  Foo base;                         // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { }     // Bar::~Bar
int aBar(Bar* self) { return 2; }   // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
  &vtableFoo,                       // can dynamic_cast to Foo
  (void(*)(Foo*)) destructBar,      // must cast type to avoid errors
  (int(*)(Foo*)) aBar
};
void constructBar(Bar* self) {      // Bar::Bar()
  self->base.vtable = &vtableBar;   // point to Bar vtable
}

fonction f exécution d'un appel de fonction virtuelle

void f(Foo* arg) {                  // same functionality as above
  Foo x; constructFoo(&x); aFoo(&x);
  Bar y; constructBar(&y); aBar(&y);
  arg->vtable->a(arg);              // virtual function call
  Foo z; copyConstructFoo(&z, arg);
  aFoo(&z);
  destructFoo(&z);
  destructBar(&y);
  destructFoo(&x);
}

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 argest de type Foo*et que vous prenez arg->vtable, mais est en fait un objet de type Bar, vous obtenez toujours l'adresse correcte du vtable. En effet, le vtableest toujours le premier élément à l'adresse de l'objet, qu'il soit appelé vtableou base.vtabledans une expression correctement typée.

MvG
la source
"Chaque objet d'une classe polymorphe pointera vers sa propre vtable." Êtes-vous en train de dire que chaque objet a sa propre table virtuelle? La vtable AFAIK est partagée entre tous les objets de la même classe. Faites-moi savoir si je me trompe.
Bhuwan
1
@Bhuwan: Non, vous avez raison: il n'y a qu'une seule vtable par type (qui peut être par instanciation de template dans le cas de templates). Je voulais dire que chaque objet d'une classe polymorphe avec point sur la vtable qui lui est applicable, donc chaque objet a un tel pointeur, mais pour les objets du même type, il pointera vers la même table. Je devrais probablement reformuler cela.
MvG
1
@MvG " objets du même type il pointera vers la même table " pas pendant la construction des classes de base avec des classes de base virtuelles! (un cas très spécial)
curiousguy
1
@curiousguy: Je classerais cela sous «ce qui précède est simplifié à bien des égards», d'autant plus que l'application principale des bases virtuelles est l'héritage multiple, que je n'ai pas non plus modélisé. Mais merci pour le commentaire, il est utile d'avoir ceci ici pour les personnes qui pourraient avoir besoin de plus de profondeur.
MvG
3

Habituellement, avec un VTable, un tableau de pointeurs vers des fonctions.

Lou Franco
la source
2

Cette réponse a été intégrée à la réponse du wiki communautaire

  • Les classes abstraites ont-elles simplement un NULL pour le pointeur de fonction d'au moins une entrée?

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.

Michael Burr
la source
De plus, je ne pense pas qu'une classe abstraite puisse définir une implémentation pour une fonction virtuelle pure. Par définition, une fonction virtuelle pure n'a pas de corps (par exemple bool my_func () = 0;). Vous pouvez cependant fournir des implémentations pour les fonctions virtuelles normales.
Zach Burlingame
Une fonction virtuelle pure peut avoir une définition. Voir l'article n ° 34 de Scott Meyers "Effective C ++, 3rd Ed", ISO 14882-2003 10.4-2, ou bytes.com/forum/thread572745.html
Michael Burr
2

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

class Foo
{
protected:
 void(*)(Foo*) MyFunc;
public:
 Foo() { MyFunc = 0; }
 void ReplciatedVirtualFunctionCall()
 {
  MyFunc(*this);
 }
...
};

class Bar : public Foo
{
private:
 static void impl1(Foo* f)
 {
  ...
 }
public:
 Bar() { MyFunc = impl1; }
...
};

class Baz : public Foo
{
private:
 static void impl2(Foo* f)
 {
  ...
 }
public:
 Baz() { MyFunc = impl2; }
...
};
jheriko
la source
void(*)(Foo*) MyFunc;est-ce une syntaxe Java?
curiousguy
non, sa syntaxe C / C ++ pour les pointeurs de fonction. Pour me citer "Vous pouvez recréer la fonctionnalité des fonctions virtuelles en C ++ en utilisant des pointeurs de fonction". c'est un peu de syntaxe désagréable, mais quelque chose à connaître si vous vous considérez comme un programmeur C.
jheriko
Le pointeur de fonction ac ressemblerait plus à: int ( PROC) (); et un pointeur vers une fonction membre de classe ressemblerait à: int (ClassName :: MPROC) ();
Menace
1
@menace, vous avez oublié une syntaxe ici ... vous pensez peut-être au typedef? typedef int (* PROC) (); donc vous pouvez simplement faire PROC foo plus tard au lieu de int (* foo) ()?
jheriko du
2

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 classe dérivée n'ajoute pas de nouvelles fonctions virtuelles et n'en remplace aucune. Dans ce cas, cette classe partage la vtable avec la classe de base.
  • La classe dérivée ajoute et remplace les méthodes virtuelles. Dans ce cas, il obtient sa propre vtable, où les fonctions virtuelles ajoutées ont un index commençant après la dernière dérivée.
  • Plusieurs classes polymorphes dans l'héritage. Dans ce cas, nous avons un décalage d'index entre la deuxième base et la base suivante et l'index de celui-ci dans la classe dérivée

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:

  • Si vous appelez la méthode via une valeur (une variable ou le résultat d'une fonction qui renvoie une valeur) - dans ce cas, le compilateur n'a aucun doute sur la classe réelle de l'objet et peut la "résoudre" au moment de la compilation .
  • Si la méthode virtuelle est déclarée finaldans 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 comme bool Horse::HasHoof() { return true; }cela vous donnerait la possibilité d'appeler if (anim->HasHoof())qui sera plus rapide que d'essayer if(dynamic_cast<Horse*>(anim)). En effet, dynamic_castil 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.

Ethouris
la source
2

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 devoid* .

Remarque: .*et ->*sont des opérateurs différents de *et ->. Les pointeurs de fonction membre fonctionnent différemment.

#include <iostream>
#include <vector>
#include <memory>

struct vtable; // forward declare, we need just name

class animal
{
public:
    const std::string& get_name() const { return name; }

    // these will be abstract
    bool has_tail() const;
    bool has_wings() const;
    void sound() const;

protected: // we do not want animals to be created directly
    animal(const vtable* vtable_ptr, std::string name)
    : vtable_ptr(vtable_ptr), name(std::move(name)) { }

private:
    friend vtable; // just in case for non-public methods

    const vtable* const vtable_ptr;
    std::string name;
};

class cat : public animal
{
public:
    cat(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does meow\n"; 
    }
};

class dog : public animal
{
public:
    dog(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does whoof\n"; 
    }
};

class parrot : public animal
{
public:
    parrot(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return false; }
    bool has_wings() const { return true; }
    void sound() const
    {
        std::cout << get_name() << " does crrra\n"; 
    }
};

// now the magic - pointers to member functions!
struct vtable
{
    bool (animal::* const has_tail)() const;
    bool (animal::* const has_wings)() const;
    void (animal::* const sound)() const;

    // constructor
    vtable (
        bool (animal::* const has_tail)() const,
        bool (animal::* const has_wings)() const,
        void (animal::* const sound)() const
    ) : has_tail(has_tail), has_wings(has_wings), sound(sound) { }
};

// global vtable objects
const vtable vtable_cat(
    static_cast<bool (animal::*)() const>(&cat::has_tail),
    static_cast<bool (animal::*)() const>(&cat::has_wings),
    static_cast<void (animal::*)() const>(&cat::sound));
const vtable vtable_dog(
    static_cast<bool (animal::*)() const>(&dog::has_tail),
    static_cast<bool (animal::*)() const>(&dog::has_wings),
    static_cast<void (animal::*)() const>(&dog::sound));
const vtable vtable_parrot(
    static_cast<bool (animal::*)() const>(&parrot::has_tail),
    static_cast<bool (animal::*)() const>(&parrot::has_wings),
    static_cast<void (animal::*)() const>(&parrot::sound));

// set vtable pointers in constructors
cat::cat(std::string name) : animal(&vtable_cat, std::move(name)) { }
dog::dog(std::string name) : animal(&vtable_dog, std::move(name)) { }
parrot::parrot(std::string name) : animal(&vtable_parrot, std::move(name)) { }

// implement dynamic dispatch
bool animal::has_tail() const
{
    return (this->*(vtable_ptr->has_tail))();
}

bool animal::has_wings() const
{
    return (this->*(vtable_ptr->has_wings))();
}

void animal::sound() const
{
    (this->*(vtable_ptr->sound))();
}

int main()
{
    std::vector<std::unique_ptr<animal>> animals;
    animals.push_back(std::make_unique<cat>("grumpy"));
    animals.push_back(std::make_unique<cat>("nyan"));
    animals.push_back(std::make_unique<dog>("doge"));
    animals.push_back(std::make_unique<parrot>("party"));

    for (const auto& a : animals)
        a->sound();

    // note: destructors are not dispatched virtually
}
Xeverous
la source
1

Chaque objet a un pointeur vtable qui pointe vers un tableau de fonctions membres.


la source
1

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.

Philip Stuyck
la source
0

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:

class B { ~B() = 0; }; // Abstract Base class
class D : public B { ~D() {} }; // Concrete Derived class

D* pD = new D();
B* pB = pD;

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.

Andrew Stein
la source
Ce n'est pas correct pour 2 raisons: 1) une classe abstraite peut avoir des méthodes virtuelles régulières en plus des méthodes virtuelles pures, et 2) des méthodes virtuelles pures peuvent éventuellement avoir une définition qui peut être appelée avec un nom complet.
Michael Burr
À la réflexion, j'imagine que si toutes les méthodes virtuelles étaient purement virtuelles, le compilateur pourrait optimiser la vtable (il aurait besoin d'aide pour former l'éditeur de liens pour s'assurer qu'il n'y avait pas de définitions également).
Michael Burr
1
" La réponse est qu'aucune table virtuelle n'est créée du tout pour les classes abstraites. " Faux. " Ce n'est pas nécessaire car aucun objet de ces classes ne peut être créé! " Faux.
curiousguy
Je peux suivre votre raisonnement selon lequel aucune table ne 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 -Ssuivi de c++filtet il y a clairement une vtable pour y être Bincluse. 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 fichier dynamic_cast<B*>. Même -fno-rttine fait pas disparaître la vtable. Avec clang -O3au lieu de gccça, il a soudainement disparu.
MvG
@MvG " 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 " Oui, cela signifie simplement cela.
curiousguy
0

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 :

#ifndef CCPOLITE_H
#define CCPOLITE_H

/* the vtable or interface */
typedef struct {
    void (*Greet)(void *);
    void (*Thank)(void *);
} ICCPolite;

/**
 * the actual "object" literal as C++ sees it; public variables be here too 
 * all CPolite objects use(are instances of) this struct's structure.
 */
typedef struct {
    ICCPolite *vtbl;
} CPolite;

#endif /* CCPOLITE_H */

CCPolite_constructor.h :

/** 
 * unconventionally include me after defining OBJECT_NAME to automate
 * static(allocation-less) construction.
 *
 * note: I assume CPOLITE_H is included; since if I use anonymous structs
 *     for each object, they become incompatible and cause compile time errors
 *     when trying to do stuff like assign, or pass functions.
 *     this is similar to how you can't pass void * to windows functions that
 *         take handles; these handles use anonymous structs to make 
 *         HWND/HANDLE/HINSTANCE/void*/etc not automatically convertible, and
 *         require a cast.
 */
#ifndef OBJECT_NAME
    #error CCPolite> constructor requires object name.
#endif

CPolite OBJECT_NAME = {
    &CCPolite_Vtbl
};

/* ensure no global scope pollution */
#undef OBJECT_NAME

main.c :

#include <stdio.h>
#include "CCPolite.h"

// | A Greeter is capable of greeting; nothing else.
struct IGreeter
{
    virtual void Greet() = 0;
};

// | A Thanker is capable of thanking; nothing else.
struct IThanker
{
    virtual void Thank() = 0;
};

// | A Polite is something that implements both IGreeter and IThanker
// | Note that order of implementation DOES MATTER.
struct IPolite1 : public IGreeter, public IThanker{};
struct IPolite2 : public IThanker, public IGreeter{};

// | implementation if IPolite1; implements IGreeter BEFORE IThanker
struct CPolite1 : public IPolite1
{
    void Greet()
    {
        puts("hello!");
    }

    void Thank()
    {
        puts("thank you!");
    }
};

// | implementation if IPolite1; implements IThanker BEFORE IGreeter
struct CPolite2 : public IPolite2
{
    void Greet()
    {
        puts("hi!");
    }

    void Thank()
    {
        puts("ty!");
    }
};

// | imposter Polite's Greet implementation.
static void CCPolite_Greet(void *)
{
    puts("HI I AM C!!!!");
}

// | imposter Polite's Thank implementation.
static void CCPolite_Thank(void *)
{
    puts("THANK YOU, I AM C!!");
}

// | vtable of the imposter Polite.
ICCPolite CCPolite_Vtbl = {
    CCPolite_Thank,
    CCPolite_Greet    
};

CPolite CCPoliteObj = {
    &CCPolite_Vtbl
};

int main(int argc, char **argv)
{
    puts("\npart 1");
    CPolite1 o1;
    o1.Greet();
    o1.Thank();

    puts("\npart 2");    
    CPolite2 o2;    
    o2.Greet();
    o2.Thank();    

    puts("\npart 3");    
    CPolite1 *not1 = (CPolite1 *)&o2;
    CPolite2 *not2 = (CPolite2 *)&o1;
    not1->Greet();
    not1->Thank();
    not2->Greet();
    not2->Thank();

    puts("\npart 4");        
    CPolite1 *fake = (CPolite1 *)&CCPoliteObj;
    fake->Thank();
    fake->Greet();

    puts("\npart 5");        
    CPolite2 *fake2 = (CPolite2 *)fake;
    fake2->Thank();
    fake2->Greet();

    puts("\npart 6");        
    #define OBJECT_NAME fake3
    #include "CCPolite_constructor.h"
    fake = (CPolite1 *)&fake3;
    fake->Thank();
    fake->Greet();

    puts("\npart 7");        
    #define OBJECT_NAME fake4
    #include "CCPolite_constructor.h"
    fake2 = (CPolite2 *)&fake4;
    fake2->Thank();
    fake2->Greet();    

    return 0;
}

production:

part 1
hello!
thank you!

part 2
hi!
ty!

part 3
ty!
hi!
thank you!
hello!

part 4
HI I AM C!!!!
THANK YOU, I AM C!!

part 5
THANK YOU, I AM C!!
HI I AM C!!!!

part 6
HI I AM C!!!!
THANK YOU, I AM C!!

part 7
THANK YOU, I AM C!!
HI I AM C!!!!

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.

Dmitry
la source