Quel est le problème du diamant en C++ ? Comment le repérer et comment le réparer

L'héritage multiple en C++ est puissant, mais un outil délicat, qui conduit souvent à des problèmes s'il n'est pas utilisé avec précaution, des problèmes comme le problème du diamant.

Dans cet article, nous discuterons du problème du diamant, de la manière dont il découle de l'héritage multiple et de ce que vous pouvez faire pour résoudre le problème.

Héritage multiple en C++

L'héritage multiple est une fonctionnalité de la programmation orientée objet (OOP) où une sous-classe peut hériter de plusieurs superclasses. En d'autres termes, une classe enfant peut avoir plusieurs parents.

La figure ci-dessous montre une représentation graphique des héritages multiples.

Dans le diagramme ci-dessus, la classe C a pour parents la classe A et la classe B.

Si nous considérons un scénario de la vie réelle, un enfant hérite de son père et de sa mère. Ainsi, un enfant peut être représenté comme une classe dérivée avec « Père » et « Mère » comme parents. De même, nous pouvons avoir de nombreux exemples réels d'héritage multiple.

Dans l'héritage multiple, les constructeurs d'une classe héritée sont exécutés dans l'ordre dans lequel ils sont hérités. D'autre part, les destructeurs sont exécutés dans l'ordre inverse de leur héritage.

Illustrons maintenant l'héritage multiple et vérifions l'ordre de construction et de destruction des objets.

Illustration de code d'héritage multiple

Pour l'illustration de l'héritage multiple, nous avons exactement programmé la représentation ci-dessus en C++. Le code du programme est donné ci-dessous.

 #include<iostream>
using namespace std;
class A //base class A with constructor and destructor
{
public:
A() { cout << "class A::Constructor" << endl; }
~A() { cout << "class A::Destructor" << endl; }
};
class B //base class B with constructor and destructor
{
public:
B() { cout << "class B::Constructor" << endl; }
~B() { cout << "class B::Destructor" << endl; }
};
class C: public B, public A //derived class C inherits class A and then class B (note the order)
{
public:
C() { cout << "class C::Constructor" << endl; }
~C() { cout << "class C::Destructor" << endl; }
};
int main(){
C c;
return 0;
}

La sortie que nous obtenons du programme ci-dessus est la suivante :

 class B::Constructor
class A::Constructor
class C::Constructor
class C::Destructor
class A::Destructor
class B::Destructor

Maintenant, si nous vérifions la sortie, nous voyons que les constructeurs sont appelés dans l'ordre B, A et C tandis que les destructeurs sont dans l'ordre inverse. Maintenant que nous connaissons les bases de l'héritage multiple, nous passons au problème du diamant.

Le problème du diamant, expliqué

Le problème Diamond se produit lorsqu'une classe enfant hérite de deux classes parent qui partagent toutes deux une classe grand-parent commune. Ceci est illustré dans le schéma ci-dessous :

Ici, nous avons une classe Enfant héritant des classes Père et Mère . Ces deux classes, à leur tour, héritent de la classe Personne parce que le Père et la Mère sont tous deux Personne.

Comme le montre la figure, la classe Enfant hérite deux fois des traits de la classe Personne : une fois de Père et une fois de Mère. Cela donne lieu à une ambiguïté car le compilateur ne parvient pas à comprendre la voie à suivre.

Ce scénario donne lieu à un graphique d'héritage en forme de losange et est connu sous le nom de « Le problème du diamant ».

Illustration du code du problème du diamant

Ci-dessous, nous avons représenté l'exemple ci-dessus d'héritage en forme de losange par programmation. Le code est donné ci-dessous :

 #include<iostream>
using namespace std;
class Person { //class Person
public:
Person(int x) { cout << "Person::Person(int) called" << endl; }
};

class Father : public Person { //class Father inherits Person
public:
Father(int x):Person(x) {
cout << "Father::Father(int) called" << endl;
}
};

class Mother : public Person { //class Mother inherits Person
public:
Mother(int x):Person(x) {
cout << "Mother::Mother(int) called" << endl;
}
};

class Child : public Father, public Mother { //Child inherits Father and Mother
public:
Child(int x):Mother(x), Father(x) {
cout << "Child::Child(int) called" << endl;
}
};

int main() {
Child child(30);
}

Voici le résultat de ce programme :

 Person::Person(int) called
Father::Father(int) called
Person::Person(int) called
Mother::Mother(int) called
Child::Child(int) called

Maintenant, vous pouvez voir l'ambiguïté ici. Le constructeur de classe Person est appelé deux fois : une fois lorsque l'objet de classe Père est créé et ensuite lorsque l'objet de classe Mère est créé. Les propriétés de la classe Person sont héritées deux fois, ce qui crée une ambiguïté.

Étant donné que le constructeur de classe Person est appelé deux fois, le destructeur sera également appelé deux fois lorsque l'objet de classe Child est détruit.

Maintenant, si vous avez bien compris le problème, discutons de la solution au problème du diamant.

Comment résoudre le problème de diamant en C++

La solution au problème du diamant est d'utiliser le mot-clé virtual . Nous transformons les deux classes parent (qui héritent de la même classe grand-parent) en classes virtuelles afin d'éviter deux copies de la classe grand-parent dans la classe enfant.

Modifions l'illustration ci-dessus et vérifions la sortie :

Illustration de code pour résoudre le problème du diamant

 #include<iostream>
using namespace std;
class Person { //class Person
public:
Person() { cout << "Person::Person() called" << endl; } //Base constructor
Person(int x) { cout << "Person::Person(int) called" << endl; }
};

class Father : virtual public Person { //class Father inherits Person
public:
Father(int x):Person(x) {
cout << "Father::Father(int) called" << endl;
}
};

class Mother : virtual public Person { //class Mother inherits Person
public:
Mother(int x):Person(x) {
cout << "Mother::Mother(int) called" << endl;
}
};

class Child : public Father, public Mother { //class Child inherits Father and Mother
public:
Child(int x):Mother(x), Father(x) {
cout << "Child::Child(int) called" << endl;
}
};

int main() {
Child child(30);
}

Ici, nous avons utilisé le mot-clé virtual lorsque les classes Father et Mother héritent de la classe Person. C'est ce qu'on appelle généralement « l'héritage virtuel », qui garantit qu'une seule instance de la classe héritée (dans ce cas, la classe Person) est transmise.

En d'autres termes, la classe Child aura une seule instance de la classe Person, partagée par les classes Father et Mother. En ayant une seule instance de la classe Person, l'ambiguïté est résolue.

La sortie du code ci-dessus est donnée ci-dessous:

 Person::Person() called
Father::Father(int) called
Mother::Mother(int) called
Child::Child(int) called

Ici, vous pouvez voir que le constructeur de la classe Person n'est appelé qu'une seule fois.

Une chose à noter à propos de l'héritage virtuel est que même si le constructeur paramétré de la classe Person est explicitement appelé par les constructeurs des classes Father et Mother via les listes d'initialisation, seul le constructeur de base de la classe Person sera appelé .

C'est parce qu'il n'y a qu'une seule instance d'une classe de base virtuelle qui est partagée par plusieurs classes qui en héritent.

Pour empêcher le constructeur de base de s'exécuter plusieurs fois, le constructeur d'une classe de base virtuelle n'est pas appelé par la classe qui en hérite. Au lieu de cela, le constructeur est appelé par le constructeur de la classe concrète.

Dans l'exemple ci-dessus, la classe Child appelle directement le constructeur de base de la classe Person.

Connexes : Guide du débutant sur la bibliothèque de modèles standard en C++

Que faire si vous devez exécuter le constructeur paramétré de la classe de base ? Vous pouvez le faire en l'appelant explicitement dans la classe Child plutôt que dans les classes Father ou Mother.

Le problème du diamant en C++, résolu

Le problème Diamond est une ambiguïté qui survient dans l'héritage multiple lorsque deux classes parent héritent de la même classe grand-parent et que les deux classes parent sont héritées par une seule classe enfant. Sans utiliser l'héritage virtuel, la classe enfant hériterait deux fois des propriétés de la classe grand-parent, ce qui entraînerait une ambiguïté.

Cela peut survenir fréquemment dans le code du monde réel, il est donc important de résoudre cette ambiguïté chaque fois qu'elle est repérée.

Le problème Diamond est résolu à l'aide de l'héritage virtuel, dans lequel le mot-clé virtual est utilisé lorsque les classes parent héritent d'une classe grand-parent partagée. Ce faisant, une seule copie de la classe grand-parent est créée et la construction de l'objet de la classe grand-parent est effectuée par la classe enfant.