class A { public: void eat(){ cout<<"A";} };
class B: virtual public A { public: void eat(){ cout<<"B";} };
class C: virtual public A { public: void eat(){ cout<<"C";} };
class D: public B,C { public: void eat(){ cout<<"D";} };
int main(){
A *a = new D();
a->eat();
}
Entiendo el problema del diamante, y el código anterior no tiene ese problema.
¿Cómo resuelve exactamente el problema la herencia virtual?
Lo que entiendo:
cuando digo A *a = new D();
, el compilador quiere saber si un objeto de tipo D
se puede asignar a un puntero de tipo A
, pero tiene dos caminos que puede seguir, pero no puede decidir por sí mismo.
Entonces, ¿cómo resuelve la herencia virtual el problema (ayude al compilador a tomar la decisión)?
B
oC
en su lugar? ¡Gracias!Las instancias de clases derivadas "contienen" instancias de clases base, por lo que se ven así en la memoria:
Por lo tanto, sin herencia virtual, la instancia de la clase D se vería así:
Por lo tanto, observe dos "copias" de los datos A. La herencia virtual significa que dentro de la clase derivada hay un puntero vtable configurado en tiempo de ejecución que apunta a los datos de la clase base, por lo que las instancias de las clases B, C y D se ven así:
fuente
¿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 deD
), resolviendo así la ambigüedad. Sin embargo, esto no me dio una comprensión completa del proceso, terminé con más preguntas comoB
eC
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é instanciaA
se elegirá para formar parteD
?B
, pero virtual paraC
? ¿Es suficiente para crear una sola instancia deA
inD
?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:
Veamos la salida. La ejecución
B b(2);
creaA(2)
como se esperaba, lo mismo paraC c(3);
:D d(2, 3);
necesita ambosB
yC
, cada uno de ellos creando el suyoA
, por lo que tenemos dobleA
end
: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:Virtualidad
Ahora agreguemos herencia virtual. Usando el mismo ejemplo de código con los siguientes cambios:
Saltemos a la creación de
d
:Puede ver que
A
se crea con el constructor predeterminado ignorando los parámetros pasados de los constructores deB
yC
. Una vez que la ambigüedad desaparece, todas las llamadasgetX()
devuelven el mismo valor:Pero, ¿y si queremos llamar al constructor parametrizado
A
? Se puede hacer llamándolo explícitamente desde el constructor deD
: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 heredadaB
ahora es responsable de crearA
por sí misma, yaB
que no lo hará automáticamente.Con esta afirmación en mente, es fácil responder todas las preguntas que tenía:
D
creación niB
niC
es responsable de los parámetros deA
, depende totalmente deD
solo.C
delegará la creación deA
aD
, peroB
creará su propia instancia deA
devolver así el problema del diamantefuente
El problema no es la ruta que debe seguir el compilador. El problema es el punto final de ese camino: el resultado del reparto. Cuando se trata de conversiones de tipos, la ruta no importa, solo importa el resultado final.
Si usa herencia ordinaria, cada ruta tiene su propio punto final distintivo, lo que significa que el resultado de la conversión es ambiguo, que es el problema.
Si usa la herencia virtual, obtiene una jerarquía en forma de diamante: ambas rutas conducen al mismo punto final. En este caso, el problema de elegir el camino ya no existe (o, más precisamente, ya no importa), porque ambos caminos conducen al mismo resultado. El resultado ya no es ambiguo, eso es lo que importa. El camino exacto no lo hace.
fuente
En realidad, el ejemplo debería ser el siguiente:
... de esa manera la salida será la correcta: "EAT => D"
¡La herencia virtual solo resuelve la duplicación del abuelo! PERO todavía necesita especificar los métodos para que sean virtuales para que los métodos se anulen correctamente ...
fuente