¿Por qué otra respuesta?
Bueno, muchas publicaciones en SO y artículos externos dicen que el problema del diamante se resuelve creando una sola instancia de en Alugar de dos (una para cada padre de D), resolviendo así la ambigüedad. Sin embargo, esto no me dio una comprensión completa del proceso, terminé con más preguntas como
- ¿Qué pasa si
Be Cintenta crear diferentes instancias de, Apor ejemplo, llamar a un constructor parametrizado con diferentes parámetros ( D::D(int x, int y): C(x), B(y) {})? ¿De qué instancia Ase elegirá para formar parte D?
- ¿Qué pasa si uso herencia no virtual para
B, pero virtual para C? ¿Es suficiente para crear una sola instancia de Ain D?
- ¿Debería utilizar siempre la herencia virtual por defecto a partir de ahora como medida preventiva, ya que resuelve un posible problema de diamantes con un coste de rendimiento menor y sin otros inconvenientes?
No poder predecir el comportamiento sin probar ejemplos de código significa no comprender el concepto. A continuación se muestra lo que me ayudó a comprender la herencia virtual.
Doble a
Primero, comencemos con este código sin herencia virtual:
#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;
}
Veamos la salida. La ejecución B b(2);crea A(2)como se esperaba, lo mismo para 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);necesita ambos By C, cada uno de ellos creando el suyo A, por lo que tenemos doble Aen d:
Create d(2,3):
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3)
Esa es la razón para d.getX()causar un error de compilación, ya que el compilador no puede elegir para qué Ainstancia debe llamar al método. Aún así, es posible llamar a métodos directamente para la clase principal elegida:
d.B::getX() = 3
d.C::getX() = 2
Virtualidad
Ahora agreguemos herencia virtual. Usando el mismo ejemplo de código con los siguientes cambios:
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
...
Saltemos a la creación de d:
Create d(2,3):
A::A() C::C(2) B::B(3) D::D(2, 3)
Puede ver que Ase crea con el constructor predeterminado ignorando los parámetros pasados de los constructores de By C. Una vez que la ambigüedad desaparece, todas las llamadas getX()devuelven el mismo valor:
d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42
Pero, ¿y si queremos llamar al constructor parametrizado A? Se puede hacer llamándolo explícitamente desde el constructor de D:
D(int x, int y, int z): A(x), C(y), B(z)
Normalmente, la clase puede usar explícitamente solo constructores de padres directos, pero hay una exclusión para el caso de herencia virtual. Descubrir esta regla me hizo "clic" y me ayudó a comprender mucho las interfaces virtuales:
El código class B: virtual Asignifica que cualquier clase heredada Bahora es responsable de crear Apor sí misma, ya Bque no lo hará automáticamente.
Con esta afirmación en mente, es fácil responder todas las preguntas que tenía:
- Durante la
Dcreación ni Bni Ces responsable de los parámetros de A, depende totalmente de Dsolo.
Cdelegará la creación de Aa D, pero Bcreará su propia instancia de Adevolver así el problema del diamante
- Definir los parámetros de la clase base en la clase del nieto en lugar de la clase del hijo directo no es una buena práctica, por lo que debe tolerarse cuando exista un problema de diamantes y esta medida sea inevitable.