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 A
au 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
B
et si et C
essayait de créer différentes instances, A
par 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 A
sera 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 A
in 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 B
et C
, chacun d'eux créant le sien A
, nous avons donc le double A
de 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' A
instance 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, A
est créé avec le constructeur par défaut en ignorant les paramètres transmis par les constructeurs de B
et 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 A
signifie que toute classe héritée de B
est désormais responsable de la création A
par elle-même, car elle B
ne le fera pas automatiquement.
Avec cette déclaration à l'esprit, il est facile de répondre à toutes mes questions:
- Pendant la
D
création, B
ni C
n'est responsable des paramètres de A
, c'est totalement à D
seulement.
C
déléguera la création de A
à D
, mais B
créera sa propre instance A
pour 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.