Warum noch eine Antwort?
Nun, viele Posts auf SO und Artikel außerhalb sagen, dass das Diamantproblem gelöst wird, indem eine einzelne Instanz A
anstelle von zwei erstellt wird (eine für jedes Elternteil von D
), wodurch Mehrdeutigkeiten aufgelöst werden. Dies gab mir jedoch kein umfassendes Verständnis des Prozesses, und ich bekam noch mehr Fragen wie
- Was ist, wenn
B
und C
versucht, verschiedene Instanzen von A
z. B. dem Aufrufen eines parametrisierten Konstruktors mit verschiedenen Parametern ( D::D(int x, int y): C(x), B(y) {}
) zu erstellen ? Welche Instanz von A
wird ausgewählt, um Teil davon zu werden D
?
- Was ist, wenn ich nicht-virtuelle Vererbung für
B
, aber virtuelle Vererbung für verwende C
? Reicht es aus, eine einzelne Instanz von A
in zu erstellen D
?
- Sollte ich von nun an standardmäßig immer die virtuelle Vererbung als vorbeugende Maßnahme verwenden, da sie mögliche Diamantprobleme mit geringen Leistungskosten und ohne weitere Nachteile löst?
Das Verhalten nicht vorhersagen zu können, ohne Codebeispiele auszuprobieren, bedeutet, das Konzept nicht zu verstehen. Das Folgende hat mir geholfen, mich mit der virtuellen Vererbung zu befassen.
Doppel a
Beginnen wir zunächst mit diesem Code ohne virtuelle Vererbung:
#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;
}
Gehen wir die Ausgabe durch. Das Ausführen B b(2);
erstellt A(2)
wie erwartet, dasselbe für 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);
braucht beide B
und C
, jeder von ihnen schafft seine eigenen A
, so haben wir doppelt A
in d
:
Create d(2,3):
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3)
Dies ist der Grund für d.getX()
den Kompilierungsfehler, da der Compiler nicht auswählen kann, für welche A
Instanz er die Methode aufrufen soll. Es ist jedoch möglich, Methoden direkt für die ausgewählte übergeordnete Klasse aufzurufen:
d.B::getX() = 3
d.C::getX() = 2
Virtualität
Fügen wir nun die virtuelle Vererbung hinzu. Verwenden des gleichen Codebeispiels mit den folgenden Änderungen:
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
...
Springen wir zur Erstellung von d
:
Create d(2,3):
A::A() C::C(2) B::B(3) D::D(2, 3)
Sie können sehen, A
wird mit dem Standardkonstruktor erstellt, der die von den Konstruktoren von B
und übergebenen Parameter ignoriert C
. Da die Mehrdeutigkeit weg ist, geben alle Aufrufe getX()
denselben Wert zurück:
d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42
Aber was ist, wenn wir einen parametrisierten Konstruktor aufrufen wollen A
? Dies kann durch expliziten Aufruf vom Konstruktor von D
:
D(int x, int y, int z): A(x), C(y), B(z)
Normalerweise kann die Klasse explizit nur Konstruktoren direkter Eltern verwenden, es gibt jedoch einen Ausschluss für den Fall der virtuellen Vererbung. Das Entdecken dieser Regel hat für mich "geklickt" und mir geholfen, virtuelle Schnittstellen zu verstehen:
Code class B: virtual A
bedeutet, dass jede Klasse, von der geerbt wurde, B
jetzt selbst erstellt A
werden muss, da dies B
nicht automatisch erfolgt.
Mit dieser Aussage ist es einfach, alle Fragen zu beantworten, die ich hatte:
- Während der
D
Erstellung ist weder für B
noch C
verantwortlich für Parameter von A
, es liegt ganz bei D
nur.
C
wird die Erstellung von A
an delegieren D
, aber B
eine eigene Instanz erstellen A
, um das Diamantproblem zurückzubringen
- Das Definieren von Basisklassenparametern in der Enkelklasse anstelle eines direkten Kindes ist keine gute Vorgehensweise. Daher sollte dies toleriert werden, wenn ein Diamantproblem vorliegt und diese Maßnahme unvermeidbar ist.