Table des méthodes virtuelles - Virtual method table

Une table des méthodes virtuelles ( VMT ), table de fonctions virtuelles , table de communication virtuelle , table de distribution , vtable ou vftable est un mécanisme utilisé dans un langage de programmation de support envoi de dynamique (ou run-time méthode de liaison ).

Chaque fois qu'une classe définit une fonction (ou méthode) virtuelle , la plupart des compilateurs ajoutent une variable membre masquée à la classe qui pointe vers un tableau de pointeurs vers des fonctions (virtuelles) appelées table de méthodes virtuelles. Ces pointeurs sont utilisés au moment de l'exécution pour appeler les implémentations de fonction appropriées, car au moment de la compilation, il se peut qu'on ne sache pas encore si la fonction de base doit être appelée ou une fonction dérivée implémentée par une classe qui hérite de la classe de base.

Il existe de nombreuses manières différentes d'implémenter une telle répartition dynamique, mais l'utilisation de tables de méthodes virtuelles est particulièrement courante parmi C++ et les langages associés (tels que D et C# ). Les langages qui séparent l'interface de programmation des objets de l'implémentation, comme Visual Basic et Delphi , ont également tendance à utiliser cette approche, car elle permet aux objets d'utiliser une implémentation différente simplement en utilisant un ensemble différent de pointeurs de méthode.

Supposons qu'un programme contienne trois classes dans une hiérarchie d' héritage : une superclasse , Cat, et deux sous - classes , HouseCatet Lion. La classe Catdéfinit une fonction virtuelle nommée speak, donc ses sous-classes peuvent fournir une implémentation appropriée (par exemple soit meowou roar). Lorsque le programme appelle la speakfonction sur une Catréférence (qui peut faire référence à une instance de Cat, ou une instance de HouseCatou Lion), le code doit être capable de déterminer à quelle implémentation de la fonction l'appel doit être envoyé . Cela dépend de la classe réelle de l'objet, pas de la classe de la référence à celui-ci ( Cat). La classe ne peut généralement pas être déterminée de manière statique (c'est-à-dire au moment de la compilation ), donc le compilateur ne peut pas non plus décider quelle fonction appeler à ce moment-là. L'appel doit être envoyé à la bonne fonction dynamiquement (c'est-à-dire au moment de l'exécution ) à la place.

Mise en œuvre

La table de méthodes virtuelles d'un objet contiendra les adresses des méthodes liées dynamiquement de l'objet. Les appels de méthode sont effectués en récupérant l'adresse de la méthode dans la table des méthodes virtuelles de l'objet. La table des méthodes virtuelles est la même pour tous les objets appartenant à la même classe, et est donc généralement partagée entre eux. Les objets appartenant à des classes compatibles avec le type (par exemple les frères et sœurs dans une hiérarchie d'héritage) auront des tables de méthodes virtuelles avec la même disposition : l'adresse d'une méthode donnée apparaîtra au même décalage pour toutes les classes compatibles avec le type. Ainsi, récupérer l'adresse de la méthode à partir d'un décalage donné dans une table de méthodes virtuelles obtiendra la méthode correspondant à la classe réelle de l'objet.

Les normes C++ n'imposent pas exactement comment la répartition dynamique doit être implémentée, mais les compilateurs utilisent généralement des variations mineures sur le même modèle de base.

En règle générale, le compilateur crée une table de méthodes virtuelles distincte pour chaque classe. Lorsqu'un objet est créé, un pointeur vers cette table, appelé pointeur de table virtuelle , vpointer ou VPTR , est ajouté en tant que membre masqué de cet objet. En tant que tel, le compilateur doit également générer du code "caché" dans les constructeurs de chaque classe pour initialiser le pointeur de table virtuelle d'un nouvel objet vers l'adresse de la table de méthodes virtuelles de sa classe.

De nombreux compilateurs placent le pointeur de table virtuelle comme dernier membre de l'objet ; d'autres compilateurs le placent en premier ; le code source portable fonctionne dans les deux cas. Par exemple, g++ plaçait auparavant le pointeur à la fin de l'objet.

Exemple

Considérez les déclarations de classe suivantes dans la syntaxe C++ :

class B1 {
public:
  virtual ~B1() {}
  void f0() {}
  virtual void f1() {}
  int int_in_b1;
};

class B2 {
public:
  virtual ~B2() {}
  virtual void f2() {}
  int int_in_b2;
};

utilisé pour dériver la classe suivante :

class D : public B1, public B2 {
public:
  void d() {}
  void f2() override {}
  int int_in_d;
};

et le morceau de code C++ suivant :

B2 *b2 = new B2();
D  *d  = new D();

g++ 3.4.6 de GCC produit la disposition de mémoire 32 bits suivante pour l'objet b2:

b2:
  +0: pointer to virtual method table of B2
  +4: value of int_in_b2

virtual method table of B2:
  +0: B2::f2()   

et la disposition de mémoire suivante pour l'objet d:

d:
  +0: pointer to virtual method table of D (for B1)
  +4: value of int_in_b1
  +8: pointer to virtual method table of D (for B2)
 +12: value of int_in_b2
 +16: value of int_in_d

Total size: 20 Bytes.

virtual method table of D (for B1):
  +0: B1::f1()  // B1::f1() is not overridden

virtual method table of D (for B2):
  +0: D::f2()   // B2::f2() is overridden by D::f2()

Notez que les fonctions ne portant pas le mot-clé virtualdans leur déclaration (telles que f0()et d()) n'apparaissent généralement pas dans la table des méthodes virtuelles. Il existe des exceptions pour des cas particuliers tels que posés par le constructeur par défaut .

Notez également les destructeurs virtuels dans les classes de base, B1et B2. Ils sont nécessaires pour s'assurer qu'ils delete dpeuvent libérer de la mémoire non seulement pour D, mais aussi pour B1et B2, si dest un pointeur ou une référence aux types B1ou B2. Ils ont été exclus des dispositions de mémoire pour garder l'exemple simple.

Le remplacement de la méthode f2()dans la classe Dest implémenté en dupliquant la table des méthodes virtuelles de B2et en remplaçant le pointeur vers B2::f2()par un pointeur vers D::f2().

Héritage multiple et thunks

Le compilateur g++ implémente l' héritage multiple des classes B1et B2en classe en Dutilisant deux tables de méthodes virtuelles, une pour chaque classe de base. (Il existe d'autres façons d'implémenter l'héritage multiple, mais c'est la plus courante.) Cela conduit à la nécessité de "corrections de pointeurs", également appelées thunks , lors du transtypage .

Considérez le code C++ suivant :

D  *d  = new D();
B1 *b1 = d;
B2 *b2 = d;

While det b1pointera vers le même emplacement mémoire après l'exécution de ce code, b2pointera vers l'emplacement d+8(huit octets au-delà de l'emplacement mémoire de d). Ainsi, b2pointe vers la région à l'intérieur dqui "ressemble" à une instance de B2, c'est-à-dire qu'elle a la même disposition de mémoire qu'une instance de B2.

Invocation

Un appel à d->f1()est géré en déréférençant dle D::B1pointeur v de , en recherchant l' f1entrée dans la table des méthodes virtuelles, puis en déréférenciant ce pointeur pour appeler le code.

Dans le cas d'un seul héritage (ou dans un langage avec un seul héritage), si le pointeur v est toujours le premier élément d(comme c'est le cas avec de nombreux compilateurs), cela se réduit au pseudo-C++ suivant :

(*((*d)[0]))(d)

Où *d fait référence à la table des méthodes virtuelles de D et [0] fait référence à la première méthode de la table des méthodes virtuelles. Le paramètre d devient le pointeur « this » vers l'objet.

Dans le cas plus général, appeler B1::f1()ou D::f2()est plus compliqué :

(*(*(d[+0]/*pointer to virtual method table of D (for B1)*/)[0]))(d)   /* Call d->f1() */
(*(*(d[+8]/*pointer to virtual method table of D (for B2)*/)[0]))(d+8) /* Call d->f2() */

L'appel à d->f1() passe un pointeur B1 en paramètre. L'appel à d->f2() passe un pointeur B2 en paramètre. Ce deuxième appel nécessite une correction pour produire le pointeur correct. L'emplacement de B2::f2 n'est pas dans la table des méthodes virtuelles pour D.

Par comparaison, un appel à d->f0()est beaucoup plus simple :

(*B1::f0)(d)

Efficacité

Un appel virtuel nécessite au moins un déréférencement indexé supplémentaire et parfois un ajout de "correction", par rapport à un appel non virtuel, qui est simplement un saut vers un pointeur compilé. Par conséquent, l'appel de fonctions virtuelles est intrinsèquement plus lent que l'appel de fonctions non virtuelles. Une expérience réalisée en 1996 indique qu'environ 6 à 13 % du temps d'exécution est consacré à la simple répartition vers la fonction correcte, bien que la surcharge puisse atteindre 50 %. Le coût des fonctions virtuelles peut ne pas être aussi élevé sur les architectures de processeurs modernes en raison de caches beaucoup plus grands et d'une meilleure prédiction de branche .

De plus, dans les environnements où la compilation JIT n'est pas utilisée, les appels de fonction virtuelle ne peuvent généralement pas être intégrés . Dans certains cas, il peut être possible pour le compilateur d'effectuer un processus connu sous le nom de dévirtualisation dans lequel, par exemple, la recherche et l'appel indirect sont remplacés par une exécution conditionnelle de chaque corps en ligne, mais de telles optimisations ne sont pas courantes.

Pour éviter cette surcharge, les compilateurs évitent généralement d'utiliser des tables de méthodes virtuelles chaque fois que l'appel peut être résolu au moment de la compilation .

Ainsi, l'appel à f1ci-dessus peut ne pas nécessiter de recherche dans la table car le compilateur peut être capable de dire qui dne peut contenir qu'un Dà ce stade et Dne remplace pas f1. Ou le compilateur (ou l'optimiseur) peut être capable de détecter qu'il n'y a aucune sous-classe de B1n'importe où dans le programme qui remplace f1. L'appel à B1::f1ou B2::f2ne nécessitera probablement pas de recherche dans la table car l'implémentation est spécifiée explicitement (bien qu'elle nécessite toujours la correction du pointeur 'this').

Comparaison avec les alternatives

La table de méthodes virtuelles est généralement un bon compromis de performances pour obtenir une répartition dynamique, mais il existe des alternatives, telles que la répartition d'arbre binaire , avec des performances plus élevées mais des coûts différents.

Cependant, les tables de méthodes virtuelles n'autorisent qu'un seul envoi sur le paramètre spécial "this", contrairement aux envois multiples (comme dans CLOS , Dylan ou Julia ), où les types de tous les paramètres peuvent être pris en compte dans l'envoi.

Les tables de méthodes virtuelles ne fonctionnent également que si la répartition est limitée à un ensemble connu de méthodes, elles peuvent donc être placées dans un simple tableau construit au moment de la compilation, contrairement aux langages de typage duck (tels que Smalltalk , Python ou JavaScript ).

Les langages qui fournissent l'une de ces fonctionnalités ou les deux sont souvent envoyés en recherchant une chaîne dans une table de hachage ou une autre méthode équivalente. Il existe une variété de techniques pour faire ce plus rapide (par exemple, interner / tokenizing noms de méthode, la mise en cache lookups, juste à temps la compilation ).

Voir également

Remarques

  1. ^ L' argument deG++-fdump-class-hierarchy(à partir de la version 8 :-fdump-lang-class) peut être utilisé pour vider les tables de méthodes virtuelles pour une inspection manuelle. Pour le compilateur AIX VisualAge XlC, utilisez-qdump_class_hierarchypour vider la hiérarchie des classes et la disposition des tables de fonctions virtuelles.
  2. ^ https://stackoverflow.com/questions/17960917/why-there-are-two-virtual-destructor-in-the-virtual-table-and-where-is-address-o

Les références

  • Margaret A. Ellis et Bjarne Stroustrup (1990) Le manuel de référence C++ annoté. Reading, MA : Addison-Wesley. ( ISBN  0-201-51459-1 )