¿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 A
lugar 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
B
e C
intenta crear diferentes instancias de, A
por ejemplo, llamar a un constructor parametrizado con diferentes parámetros ( D::D(int x, int y): C(x), B(y) {}
)? ¿De qué instancia A
se 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 A
in 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 B
y C
, cada uno de ellos creando el suyo A
, por lo que tenemos doble A
en 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é A
instancia 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 A
se crea con el constructor predeterminado ignorando los parámetros pasados de los constructores de B
y 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 A
significa que cualquier clase heredada B
ahora es responsable de crear A
por sí misma, ya B
que no lo hará automáticamente.
Con esta afirmación en mente, es fácil responder todas las preguntas que tenía:
- Durante la
D
creación ni B
ni C
es responsable de los parámetros de A
, depende totalmente de D
solo.
C
delegará la creación de A
a D
, pero B
creará su propia instancia de A
devolver 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.