¿Cómo resuelve la herencia virtual la ambigüedad del "diamante" (herencia múltiple)?

Resuelto Moeb asked hace 14 años • 5 respuestas
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 la herencia virtual el problema?

Lo que entiendo: cuando digo A *a = new D();, el compilador quiere saber si un objeto de tipo Dse 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 (ayuda al compilador a tomar la decisión)?

Moeb avatar Apr 17 '10 23:04 Moeb
Aceptado

Quieres: (Alcanzable con herencia virtual)

  A  
 / \  
B   C  
 \ /  
  D 

Y no: (Qué pasa sin herencia virtual)

A   A  
|   |
B   C  
 \ /  
  D 

La herencia virtual significa que solo habrá 1 instancia de la Aclase base, no 2.

Su tipo Dtendría 2 punteros vtable (puede verlos en el primer diagrama), uno para By otro para Cquién hereda virtualmente A. DEl tamaño del objeto aumenta porque ahora almacena 2 punteros; sin embargo, ahora sólo hay uno A.

Entonces B::Ay C::Ason iguales y por eso no puede haber llamadas ambiguas de D. Si no utilizas la herencia virtual tienes el segundo diagrama de arriba. Y cualquier llamada a un miembro de A se vuelve ambigua y es necesario especificar qué camino desea tomar.

Wikipedia tiene otro buen resumen y ejemplo aquí.

Brian R. Bondy avatar Apr 17 '2010 16:04 Brian R. Bondy

¿Por qué otra respuesta?

Bueno, muchas publicaciones sobre SO y artículos externos dicen que el problema del diamante se resuelve creando una sola instancia de Aen lugar de dos (una para cada padre de D), resolviendo así la ambigüedad. Sin embargo, esto no me dio una comprensión integral del proceso, terminé con aún más preguntas como

  1. ¿Qué pasa si Bintenta Ccrear 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?
  2. ¿Qué pasa si uso herencia no virtual B, pero virtual C? ¿Es suficiente para crear una sola instancia de Ain D?
  3. ¿Debería utilizar siempre la herencia virtual de forma predeterminada a partir de ahora como medida preventiva, ya que resuelve el posible problema de los diamantes con un costo 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 entender 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;
}

Repasemos 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, así 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 por la que d.getX()se produce un error de compilación, ya que el compilador no puede elegir Apara qué 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 la 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) 

Como puede ver, Ase crea con el constructor predeterminado ignorando los parámetros pasados ​​por los constructores de By C. Como desaparece la ambigüedad, todas las llamadas devuelven getX()el mismo valor:

d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42

Pero, ¿qué pasa 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 utilizar explícitamente constructores de padres directos únicamente, pero existe una exclusión para el caso de herencia virtual. Descubrir esta regla me hizo "clic" y me ayudó mucho a comprender las interfaces virtuales:

Código class B: virtual Asignifica que cualquier clase heredada Bahora es responsable de crearse Apor sí misma, ya que Bno lo hará automáticamente.

Con esta afirmación en mente, es fácil responder todas las preguntas que tenía:

  1. Durante Dla creación, ni Bni Ces responsable de los parámetros de A, depende totalmente de Dsolo.
  2. Cdelegará la creación de Aa D, pero Bcreará su propia instancia para Atraer de vuelta el problema de los diamantes.
  3. Definir los parámetros de la clase base en la clase nieto en lugar de en la clase hija directa no es una buena práctica, por lo que debe tolerarse cuando existe un problema de diamantes y esta medida es inevitable.
nnovich-OK avatar Jul 14 '2018 21:07 nnovich-OK

Las instancias de clases derivadas almacenan los miembros de sus clases base.

Sin herencia virtual, los diseños de la memoria se ven así (tenga en cuenta las dos copias de los Amiembros de la clase D):

class A: [A members]
class B: public A [A members|B members]
class C: public A [A members|C members]
class D: public B, public C [A members|B members|A members|C members|D members]

Con la herencia virtual, los diseños de la memoria se ven así (tenga en cuenta la copia única de los Amiembros de la clase D):

class A: [A members]
class B: virtual public A [B members|A members]
                           |         ^
                           v         |
                         virtual table B

class C: virtual public A [C members|A members]
                           |         ^
                           v         |
                         virtual table C

class D: public B, public C [B members|C members|D members|A members]
                             |         |                   ^
                             v         v                   |
                           virtual table D ----------------|

Para cada clase derivada, el compilador crea una tabla virtual que contiene punteros a los miembros de sus clases base virtuales almacenadas en la clase derivada y agrega un puntero a esa tabla virtual en la clase derivada.