Article de reference

Héritage virtuel

Diagramme de l'héritage du diamant , un problème que l'héritage virtuel tente de résoudre. L'héritage virtuel est une technique C++ qui garantit qu'une seule copie des variables...

Diagramme de l'héritage du diamant , un problème que l'héritage virtuel tente de résoudre.

L'héritage virtuel est une technique C++ qui garantit qu'une seule copie des variables membres d' une classe de base est héritée par les classes dérivées des petits-enfants. Sans héritage virtuel, si deux classes et héritent d'une classe et qu'une classe hérite à la fois de et , alors contiendra deux copies des variables membres de : une via et une via . Celles-ci seront accessibles indépendamment, en utilisant la résolution de portée . BCADBCDABC

Au lieu de cela, si les classes Bet Chéritent virtuellement de la classe A, alors les objets de la classe Dcontiendront un seul ensemble de variables membres de la classe A.

Cette fonctionnalité est particulièrement utile pour l'héritage multiple , car elle fait de la base virtuelle un sous-objet commun pour la classe dérivée et toutes les classes qui en sont dérivées. Cela peut être utilisé pour éviter le problème du diamant en clarifiant l'ambiguïté sur la classe ancêtre à utiliser, car du point de vue de la classe dérivée ( Ddans l'exemple ci-dessus), la base virtuelle ( A) agit comme s'il s'agissait de la classe de base directe de D, et non d'une classe dérivée indirectement via une base ( Bou C).

On l'utilise lorsque l'héritage représente une restriction d'un ensemble plutôt qu'une composition de parties. En C++, une classe de base destinée à être commune à toute la hiérarchie est désignée comme virtuelle par le virtual mot-clé .

Considérez la hiérarchie de classes suivante.

Héritage virtuel UML.svg

struct Animal { virtual ~ Animal () = default ; // Montre explicitement que le destructeur de classe par défaut sera créé. virtual void Eat () {} };
struct Mammifère : Animal { virtuel void Respirer () {} };
struct WingedAnimal : Animal { virtuel void Flap () {} };
// Une chauve-souris est un mammifère ailé 
struct Bat : Mammal , WingedAnimal {};

Comme indiqué ci-dessus, un appel à bat.Eatest ambigu car il existe deux Animalclasses de base (indirectes) dans Bat, donc tout Batobjet possède deux sous-objets de classe de base différents Animal. Ainsi, une tentative de liaison directe d'une référence au Animalsous-objet d'un Batobjet échouerait, car la liaison est intrinsèquement ambiguë :

Chauve -souris chauve -souris ; Animal & animal = chauve-souris ; // erreur : dans quel sous-objet Animal une chauve-souris doit-elle être convertie, // un Mammal::Animal ou un WingedAnimal::Animal ?

Pour lever l'ambiguïté, il faudrait convertir explicitement baten sous-objet de classe de base :

Chauve -souris chauve-souris ; Animal & mammifère = static_cast < Mammifère &> ( chauve-souris ); Animal & ailé = static_cast < WingedAnimal &> ( chauve-souris );

Pour appeler Eat, la même désambiguïsation ou qualification explicite est nécessaire : static_cast<Mammal&>(bat).Eat()ou static_cast<WingedAnimal&>(bat).Eat()ou alternativement bat.Mammal::Eat()et bat.WingedAnimal::Eat(). La qualification explicite utilise non seulement une syntaxe plus simple et uniforme pour les pointeurs et les objets, mais permet également une répartition statique, ce qui en fait sans doute la méthode préférable.

Dans ce cas, le double héritage de Animalest probablement indésirable, car nous voulons modéliser que la relation ( Batest un Animal) n'existe qu'une seule fois ; que a Batest un Mammalet est un WingedAnimal, n'implique pas qu'il s'agisse d'un Animaldeux fois : une Animalclasse de base correspond à un contrat qui Batimplémente (la relation " est un " ci-dessus signifie en réalité " implémente les exigences de "), et a Batn'implémente le Animalcontrat qu'une seule fois. La signification réelle de " est un une seule fois " est que Batne devrait avoir qu'une seule façon d'implémenter Eat, et non deux façons différentes, selon que la Mammalvue de the Batest eating, ou la WingedAnimalvue de the Bat. (Dans le premier exemple de code, nous voyons que Eatn'est pas surchargé dans l'un ou l' Mammalautre ou WingedAnimal, donc les deux Animalsous-objets se comporteront en fait de la même manière, mais ce n'est qu'un cas dégénéré, et cela ne fait aucune différence du point de vue C++.)

Cette situation est parfois appelée héritage en diamant (voir Problème du diamant ) car le diagramme d'héritage a la forme d'un diamant. L'héritage virtuel peut aider à résoudre ce problème.

La solution

Nous pouvons re-déclarer nos classes comme suit :

struct Animal { virtuel ~ Animal () = par défaut ; virtuel void Eat () {} };
// Deux classes héritant virtuellement de Animal : 
struct Mammal : virtual Animal { virtual void Breathe () {} };
struct WingedAnimal : animal virtuel { void virtuel Flap () {} };
// Une chauve-souris est toujours un mammifère ailé 
struct Bat : Mammal , WingedAnimal {};

La Animalpartie de Bat::WingedAnimalest maintenant la même Animal instance que celle utilisée par Bat::Mammal, ce qui revient à dire que a Batn'a qu'une seule Animalinstance partagée dans sa représentation et donc un appel à Bat::Eatest sans ambiguïté. De plus, un cast direct de Batto Animalest également sans ambiguïté, maintenant qu'il n'existe qu'une seule Animalinstance qui Batpourrait être convertie en.

La possibilité de partager une instance unique du Animalparent entre Mammalet WingedAnimalest activée en enregistrant le décalage mémoire entre les membres Mammalou et ceux de la base au sein de la classe dérivée. Cependant, ce décalage ne peut dans le cas général être connu qu'au moment de l'exécution, il doit donc devenir ( , , , , , ). Il y a deux pointeurs vtable , un par hiérarchie d'héritage qui hérite virtuellement de . Dans cet exemple, un pour et un pour . La taille de l'objet a donc augmenté de deux pointeurs, mais il n'y en a désormais qu'un et aucune ambiguïté. Tous les objets de type utiliseront les mêmes vpointers, mais chaque objet contiendra son propre objet unique. Si une autre classe hérite de , comme , alors le vpointer dans la partie de sera généralement différent du vpointer dans la partie de bien qu'ils puissent être identiques si la classe a la même taille que . WingedAnimalAnimalBatvpointerMammalvpointerWingedAnimalBatAnimalAnimalMammalWingedAnimalAnimalBatBatAnimalMammalSquirrelMammalSquirrelMammalBatSquirrelBat

Exemple supplémentaire de plusieurs ancêtres

Cet exemple illustre un cas où la classe de base Apossède une variable constructeur msget un ancêtre supplémentaire Eest dérivé de la classe petit-enfant D.

 UN / \ Colombie-Britannique \ / D | E 

Ici, Ail faut construire à la fois Det E. De plus, l'inspection de la variable msgillustre comment la classe Adevient une classe de base directe de sa classe dérivée, par opposition à une classe de base de toute classe dérivée intermédiaire entre Aet la classe dérivée finale. Le code ci-dessous peut être exploré de manière interactive ici.

#include <chaîne> #include <iostream>
classe A { privé : std :: chaîne _msg ; public : A ( std :: chaîne x ) : _msg ( x ) {} void test (){ std :: cout << "bonjour de A: " << _msg << " " ; } };
// B,C hérite virtuellement d'une 
classe B : public virtuel A { public : B () : A ( "b" ){} }; classe C : public virtuel A { public : C () : A ( "c" ){} };
// puisque B, C héritent virtuellement de A, A doit être construit dans chaque enfant 
// Les constructeurs B() et C() peuvent être omis 
class D : public B , C { public : D () : A ( "d_a" ), B (), C (){} }; // Constructeur D() omis class E : public D { public : E () : A ( "e_a" ){} };
// s'arrête sans construire A 
// classe D : public B,C { public: D():B(),C(){} };
// s'arrête sans construire A 
//classe E : public D { public: E():D(){} };
int main ( int argc , char ** argv ){ D d ; d . test (); // affiche : « bonjour de A : d_a »
E e ; e . test (); // affiche : « bonjour de A : e_a » }

Méthodes purement virtuelles

Supposons qu'une méthode virtuelle pure soit définie dans la classe de base. Si une classe dérivée hérite virtuellement de la classe de base, la méthode virtuelle pure n'a pas besoin d'être définie dans cette classe dérivée. Cependant, si la classe dérivée n'hérite pas virtuellement de la classe de base, toutes les méthodes virtuelles doivent être définies. Le code ci-dessous peut être exploré de manière interactive ici.

#include <chaîne> #include <iostream>
classe A { protégé : std :: chaîne _msg ; public : A ( std :: chaîne x ) : _msg ( x ) {} void test (){ std :: cout << "bonjour de A : " << _msg << " " ; } virtuel void pure_virtual_test () = 0 ; };
// puisque B, C héritent virtuellement de A, la méthode virtuelle pure pure_virtual_test n'a pas besoin d'être définie 
class B : virtual public A { public : B ( std :: string x ) : A ( "b" ){} }; class C : virtual public A { public : C ( std :: string x ) : A ( "c" ){} };
// puisque B, C héritent virtuellement de A, A doit être construit dans chaque enfant 
// cependant, puisque D n'hérite pas virtuellement de B, C, la méthode virtuelle pure dans A *doit être définie* 
class D : public B , C { public : D ( std :: string x ) : A ( "d_a" ), B ( "d_b" ), C ( "d_c" ){} void pure_virtual_test () override { std :: cout << "pure virtual hello from: " << _msg << " " ; } };
// il n'est pas nécessaire de redéfinir la méthode virtuelle pure après que le parent l'ait définie 
class E : public D { public : E ( std :: string x ) : A ( "e_a" ), D ( "e_d" ){} };
int main ( int argc , char ** argv ){ D d ( "d" ); d . test (); // bonjour de A : d_a d . pure_virtual_test (); // bonjour virtuel pur de : d_a
E e ( "e" ); e . test (); // bonjour de A : e_a e . pure_virtual_test (); // bonjour virtuel pur de : e_a }
C++
Original text
Rate this translation
Your feedback will be used to help improve Google Translate