ProgrammationDéveloppeur C++

Qu'est-ce que l'héritage virtuel en C++ et à quoi cela sert-il ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

Historique de la question

Dans C++, les hiérarchies de classes peuvent poser le problème du diamant (diamond problem), lorsque deux sous-classes héritent d'une même classe de base, et qu'une autre classe est ensuite créée, héritant de ces deux-là. Dans ce cas, un objet de la classe dérivée contiendra deux copies indépendantes de la classe de base. Pour résoudre ce problème, C++ a implémenté l'héritage virtuel.

Problème

Si l'on utilise l'héritage multiple classique :

class A { public: int x; }; class B : public A {}; class C : public A {}; class D : public B, public C {};

L'objet D contiendra 2 copies de A : une à travers B, l'autre à travers C. Cela entraîne des ambiguïtés lors de l'accès à A::x et une consommation mémoire injustifiée.

Solution

L'héritage virtuel élimine la duplication de la classe de base. Une seule instance de la classe de base A sera utilisée par tous les descendants :

class A { public: int x; }; class B : public virtual A {}; class C : public virtual A {}; class D : public B, public C {};

Maintenant, D ne contient qu'une seule copie de A, et l'ambiguïté avec l'accès à A::x est résolue.

Caractéristiques clés :

  • Élimine le problème du diamant (diamond problem)
  • L'héritage virtuel ralentit l'accès aux membres de la classe de base
  • Le constructeur de la classe de base virtuelle est appelé par le constructeur du "dernier" descendant

Questions pièges.

Pourquoi ne peut-on pas résoudre le problème du diamant en spécifiant explicitement le chemin via la résolution de portée ?

La résolution de portée ne résout que l'ambiguïté d'accès aux membres de la classe de base, mais elle ne supprime pas les copies supplémentaires de la classe de base elle-même. Le problème de double initialisation et de double stockage de données demeure.

Dans quel ordre les constructeurs sont-ils appelés lors de l'héritage virtuel ?

Les constructeurs des classes de base virtuelles sont appelés en premier et seulement une fois, directement par le constructeur de la classe la plus éloignée dans la hiérarchie d'héritage.

Exemple :

class A { public: A() { std::cout << "A\n"; } }; class B : public virtual A { public: B() { std::cout << "B\n"; } }; class C : public virtual A { public: C() { std::cout << "C\n"; } }; class D : public B, public C { public: D() { std::cout << "D\n"; } }; D d; // Sortie : A B C D

Faut-il déclarer l'héritage virtuel à la fois lors de la déclaration et de la définition de la classe ?

Oui, l'héritage virtuel doit être spécifié partout où il y a héritage de la classe de base concernée. Sinon, une erreur de compilation est inévitable, et l'héritage multiple deviendra classique.

Erreurs typiques et anti-patterns

  • Ne pas spécifier virtual lors de l'héritage — entraîne la création de deux instances de la classe de base
  • Essayer d'appeler le constructeur de la classe de base virtuelle dans toutes les classes intermédiaires — provoque des erreurs de compilation
  • L'abus de l'héritage virtuel complique les hiérarchies et la maintenabilité du code

Exemple de la vie réelle

Cas négatif

Une hiérarchie complexe, dans laquelle l'héritage virtuel n'est pas utilisé, ce qui entraîne deux copies des paramètres dans l'objet :

Avantages :

  • Permet une compilation et un fonctionnement faciles

Inconvénients :

  • Les données de configuration peuvent être facilement confuses, ce qui entraîne des défauts de « duplication » des données de base

Cas positif

L'héritage virtuel est utilisé avec un constructeur cohérent du descendant le plus haut :

Avantages :

  • Pas de duplication des données de base, le comportement est clair et simple

Inconvénients :

  • Une surcharge de complexité pour les développeurs sans expérience avec l'héritage virtuel peut survenir