ProgrammationDéveloppeur C++

Qu'est-ce que la table virtuelle (vtable) en C++ ? Comment le compilateur implémente-t-il le polymorphisme dynamique, et quelles sont les subtilités liées à ce mécanisme ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

En C++, le polymorphisme dynamique est mis en œuvre par le biais de mécanisme des fonctions virtuelles à l'aide d'une table spéciale — vtable (table virtuelle). Pour toute classe ayant au moins une fonction virtuelle, le compilateur génère une vtable : c'est un tableau de pointeurs vers les fonctions virtuelles de la classe. Chaque objet de cette classe contient un pointeur caché vers la vtable — appelé vptr (pointeur virtuel).

Au moment de la création d'un objet d'une classe avec des fonctions virtuelles, le vptr est initialisé à la table vtable correspondant à la classe de l'objet. Lorsqu'une fonction virtuelle est appelée, l'appel ne se fait pas directement, mais via l'adresse stockée dans la vtable, choisissant la bonne implémentation en fonction du type réel de l'objet (même lorsqu'il est référencé par le type de base).

Exemple :

class Base { public: virtual void foo() { std::cout << "Base::foo() "; } }; class Derived : public Base { public: void foo() override { std::cout << "Derived::foo() "; } }; void call_foo(Base* obj) { obj->foo(); // appel via vtable }

L'appel à foo sera un accès à la vtable : si c'est dans Derived, la fonction redéfinie sera appelée.

Subtilités :

  • Si la classe ne contient pas de fonctions virtuelles, la vtable et le vptr ne sont pas ajoutés.
  • Les méthodes virtuelles ne deviennent pas inline, perdant souvent en performance.
  • Cela augmente la taille de l'instance de la classe (de la taille du vptr — généralement 4/8 octets).
  • Les appels virtuels sont plus lents que les appels directs.

Question piège.

"Un méthode est-elle virtuelle si elle est déclarée comme virtual, mais n'est pas redéfinie dans la classe dérivée ? L'appel via vtable fonctionnera-t-il dans ce cas ?"

Réponse : Oui, si la fonction est déclarée comme virtual dans la classe de base, elle reste virtuelle pour tous les descendants, même si elle n'est pas redéfinie. L'appel via un pointeur de base se fera toutefois par la vtable. Si la méthode n'est pas redéfinie, la version de la classe de base sera appelée.

Exemple :

struct Base { virtual void foo() { std::cout << "B "; } }; struct Derived : Base { }; Base* obj = new Derived(); obj->foo(); // Affichera "B", mais appel via vtable !

Exemples d'erreurs réelles dues à l'ignorance des subtilités du sujet.


Histoire

Dans un grand projet, on a oublié de déclarer le destructeur comme virtuel dans la classe de base avec des fonctions virtuelles. Cela a conduit à des fuites de mémoire — lors de la suppression par un pointeur de base, seul le destructeur de la classe de base était appelé, pas celui de la classe dérivée.


Histoire

Dans la classe, une méthode virtuelle a été déclarée, mais a été cachée par erreur dans l'héritier (pas override, mais avec une autre signature/nom d'arguments). Les appels via la classe de base ne tombaient pas sur la fonction appropriée, ce qui entraînait un comportement incorrect.


Histoire

La mise à jour de la vtable après un héritage multiple a conduit à des erreurs : des appels incorrects de méthodes lors de l'utilisation de dynamic_cast, car le décalage du vptr dans une hiérarchie de classes complexe n'était pas pris en compte.