Pourquoi une autre réponse ?
De nombreux posts sur SO et articles à l'extérieur disent que le problème du diamant est résolu par la création d'une instance unique de A
au lieu de deux (un pour chaque parent de D
), ce qui permet de lever l'ambiguïté. Cependant, cela ne m'a pas permis d'acquérir une compréhension complète du processus, et j'ai fini par me poser encore plus de questions, comme par exemple
- et si
B
y C
tente de créer différentes instances de A
par exemple en appelant un constructeur paramétré avec différents paramètres ( D::D(int x, int y): C(x), B(y) {}
) ? Quelle instance de A
seront choisis pour faire partie de la D
?
- Que se passe-t-il si j'utilise l'héritage non virtuel pour
B
mais virtuel pour les C
? Est-ce suffisant pour créer une seule instance de A
en D
?
- Devrais-je toujours utiliser l'héritage virtuel par défaut à partir de maintenant comme mesure préventive puisqu'il résout le problème du diamant possible avec un coût de performance mineur et aucun autre inconvénient ?
Ne pas être capable de 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
Commençons par 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;
}
Passons en revue les résultats. 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 à la fois B
y C
chacun d'entre eux créant son propre A
Nous avons donc un double A
en 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()
provoquerait une erreur de compilation, car le compilateur ne peut pas choisir le type de A
instance pour laquelle il doit appeler la méthode. Il est toutefois possible d'appeler directement les méthodes de la classe mère choisie :
d.B::getX() = 3
d.C::getX() = 2
Virtualité
Ajoutons maintenant l'héritage virtuel. En utilisant le 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 un constructeur par défaut qui ignore les paramètres transmis par les constructeurs de B
y C
. L'ambiguïté étant levée, tous les appels à getX()
renvoient la même valeur :
d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42
Mais qu'en est-il si nous voulons appeler le constructeur paramétré de A
? Il est possible de le faire en l'appelant explicitement à partir du constructeur de D
:
D(int x, int y, int z): A(x), C(y), B(z)
Normalement, une classe ne peut utiliser explicitement que les constructeurs de ses parents directs, mais il existe une exclusion pour les cas d'héritage virtuel. La découverte de cette règle m'a fait "tilt" et m'a beaucoup aidé à comprendre les interfaces virtuelles :
Code class B: virtual A
signifie que toute classe héritée de B
est désormais responsable de la création de A
par lui-même, puisque B
ne le fera pas automatiquement.
Avec cette déclaration à l'esprit, il est facile de répondre à toutes les questions que je me posais :
- Pendant
D
la création non plus B
ni C
est responsable des paramètres de A
, c'est à vous de décider D
seulement.
-
C
déléguera la création de A
a D
mais B
créera sa propre instance de A
ramenant ainsi le problème du diamant
- Définir les paramètres de la classe de base dans la classe du petit-enfant plutôt que dans celle de l'enfant direct n'est pas une bonne pratique, elle doit donc être tolérée lorsqu'il existe un problème de diamant et que cette mesure est inévitable.