Pourquoi une autre réponse?
Eh bien, de nombreux articles sur SO et articles extérieurs disent que le problème du diamant est résolu en créant une seule instance de Aau lieu de deux (une pour chaque parent de D), résolvant ainsi l'ambiguïté. Cependant, cela ne m'a pas donné une compréhension complète du processus, je me suis retrouvé avec encore plus de questions comme
Bet si et Cessayait de créer différentes instances, Apar exemple, d'appeler un constructeur paramétré avec des paramètres différents ( D::D(int x, int y): C(x), B(y) {})? Quelle instance de Asera choisie pour faire partie de D?
- et si j'utilise l'héritage non virtuel pour
B, mais virtuel pour C? Est-ce suffisant pour créer une seule instance de Ain D?
- Dois-je toujours utiliser l'héritage virtuel par défaut à partir de maintenant comme mesure préventive car il résout d'éventuels problèmes de diamant avec un coût de performance mineur et aucun autre inconvénient?
Ne pas pouvoir prédire le comportement sans essayer des échantillons de code signifie ne pas comprendre le concept. Voici ce qui m'a aidé à comprendre l'héritage virtuel.
Double A
Tout d'abord, commençons avec ce code sans héritage virtuel:
#include<iostream>
using namespace std;
class A {
public:
A() { cout << "A::A() "; }
A(int x) : m_x(x) { cout << "A::A(" << x << ") "; }
int getX() const { return m_x; }
private:
int m_x = 42;
};
class B : public A {
public:
B(int x):A(x) { cout << "B::B(" << x << ") "; }
};
class C : public A {
public:
C(int x):A(x) { cout << "C::C(" << x << ") "; }
};
class D : public C, public B {
public:
D(int x, int y): C(x), B(y) {
cout << "D::D(" << x << ", " << y << ") "; }
};
int main() {
cout << "Create b(2): " << endl;
B b(2); cout << endl << endl;
cout << "Create c(3): " << endl;
C c(3); cout << endl << endl;
cout << "Create d(2,3): " << endl;
D d(2, 3); cout << endl << endl;
// error: request for member 'getX' is ambiguous
//cout << "d.getX() = " << d.getX() << endl;
// error: 'A' is an ambiguous base of 'D'
//cout << "d.A::getX() = " << d.A::getX() << endl;
cout << "d.B::getX() = " << d.B::getX() << endl;
cout << "d.C::getX() = " << d.C::getX() << endl;
}
Permet de passer par la sortie. L'exécution B b(2);crée A(2)comme prévu, idem pour C c(3);:
Create b(2):
A::A(2) B::B(2)
Create c(3):
A::A(3) C::C(3)
D d(2, 3);a besoin des deux Bet C, chacun d'eux créant le sien A, nous avons donc le double Ade d:
Create d(2,3):
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3)
C'est la raison pour laquelle d.getX()une erreur de compilation est provoquée car le compilateur ne peut pas choisir l' Ainstance pour laquelle il doit appeler la méthode. Il est toujours possible d'appeler des méthodes directement pour la classe parent choisie:
d.B::getX() = 3
d.C::getX() = 2
Virtualité
Ajoutons maintenant l'héritage virtuel. Utilisation du même exemple de code avec les modifications suivantes:
class B : virtual public A
...
class C : virtual public A
...
cout << "d.getX() = " << d.getX() << endl; //uncommented
cout << "d.A::getX() = " << d.A::getX() << endl; //uncommented
...
Passons à la création de d:
Create d(2,3):
A::A() C::C(2) B::B(3) D::D(2, 3)
Vous pouvez voir, Aest créé avec le constructeur par défaut en ignorant les paramètres transmis par les constructeurs de Bet C. Comme l'ambiguïté a disparu, tous les appels pour getX()renvoyer la même valeur:
d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42
Mais que faire si nous voulons appeler le constructeur paramétré pour A? Cela peut être fait en l'appelant explicitement depuis le constructeur de D:
D(int x, int y, int z): A(x), C(y), B(z)
Normalement, la classe peut utiliser explicitement des constructeurs de parents directs uniquement, mais il existe une exclusion pour le cas d'héritage virtuel. La découverte de cette règle a «cliqué» pour moi et m'a beaucoup aidé à comprendre les interfaces virtuelles:
Le code class B: virtual Asignifie que toute classe héritée de Best désormais responsable de la création Apar elle-même, car elle Bne le fera pas automatiquement.
Avec cette déclaration à l'esprit, il est facile de répondre à toutes mes questions:
- Pendant la
Dcréation, Bni Cn'est responsable des paramètres de A, c'est totalement à Dseulement.
Cdéléguera la création de Aà D, mais Bcréera sa propre instance Apour ramener ainsi le problème du diamant
- La définition des paramètres de classe de base dans la classe petit-enfant plutôt que dans la classe enfant directe n'est pas une bonne pratique, elle doit donc être tolérée lorsqu'un problème de diamant existe et que cette mesure est inévitable.