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

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<eat(); } 

Entiendo el problema del diamante, y la pieza de código anterior no tiene ese problema.

¿Cómo resuelve el problema la herencia virtual?

Lo que entiendo: cuando digo A *a = new D(); , el comstackdor quiere saber si un objeto de tipo D puede asignarse a un puntero de tipo A , pero tiene dos caminos que puede seguir, pero no puede decidir por sí mismo.

Entonces, ¿cómo la herencia virtual resuelve el problema (ayuda al comstackdor a tomar la decisión)?

Desea: (Alcanzable con herencia virtual)

  A / \ BC \ / D 

Y no: (¿Qué pasa sin herencia virtual?)

 AA | | BC \ / D 

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

Su tipo D tendría 2 punteros vtable (puede verlos en el primer diagtwig), uno para B y uno para C que prácticamente heredan A El tamaño del objeto D se incrementa porque almacena 2 punteros ahora; sin embargo, solo hay una A ahora.

Entonces B::A y C::A son lo mismo y no puede haber llamadas ambiguas de D Si no usa herencia virtual, tiene el segundo diagtwig de arriba. Y cualquier llamada a un miembro de A se vuelve ambigua y debe especificar qué ruta desea tomar.

Wikipedia tiene otro buen resumen y ejemplo aquí

Las instancias de clases derivadas “contienen” instancias de clases base, por lo que se ven en la memoria así:

 class A: [A fields] class B: [A fields | B fields] class C: [A fields | C fields] 

Por lo tanto, sin herencia virtual, la instancia de la clase D se vería así:

 class D: [A fields | B fields | A fields | C fields | D fields] '- derived from B -' '- derived from C -' 

Por lo tanto, tenga en cuenta dos “copias” de datos A. La herencia virtual significa que dentro de la clase derivada hay un puntero vtable establecido en el tiempo de ejecución que apunta a los datos de la clase base, de modo que las instancias de las clases B, C y D se vean así:

 class B: [A fields | B fields] ^---------- pointer to A class C: [A fields | C fields] ^---------- pointer to A class D: [A fields | B fields | C fields | D fields] ^---------- pointer to B::A ^--------------------- pointer to C::A 

El problema no es la ruta que el comstackdor debe seguir. El problema es el punto final de esa ruta: el resultado del reparto. Cuando se trata de escribir conversiones, la ruta no importa, solo el resultado final sí.

Si usa herencia ordinaria, cada ruta tiene su propio punto final distintivo, lo que significa que el resultado del reparto es ambiguo, que es el problema.

Si usa herencia virtual, obtiene una jerarquía en forma de diamante: ambas rutas conducen al mismo punto final. En este caso, el problema de elegir la ruta ya no existe (o, más precisamente, ya no importa), ya que ambas rutas conducen al mismo resultado. El resultado ya no es ambiguo, eso es lo que importa. La ruta exacta no.

En realidad, el ejemplo debería ser el siguiente:

 #include  //THE DIAMOND PROBLEM SOLVED!!! class A { public: virtual ~A(){ } virtual void eat(){ std::cout<<"EAT=>A";} }; class B: virtual public A { public: virtual ~B(){ } virtual void eat(){ std::cout<<"EAT=>B";} }; class C: virtual public A { public: virtual ~C(){ } virtual void eat(){ std::cout<<"EAT=>C";} }; class D: public B,C { public: virtual ~D(){ } virtual void eat(){ std::cout<<"EAT=>D";} }; int main(int argc, char ** argv){ A *a = new D(); a->eat(); delete a; } 

… de esa manera la salida será la correcta: “EAT => D”

¡La herencia virtual solo resuelve la duplicación del abuelo! PERO aún necesita especificar los métodos para ser virtual a fin de obtener los métodos correctamente anulados …

¿Por qué otra respuesta?

Bueno, muchas publicaciones sobre SO y artículos externos dicen que el problema del diamante se resuelve creando una instancia única de 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

  1. ¿Qué B si B y C intentan crear diferentes instancias de A por ejemplo, llamando al constructor parametrizado con diferentes parámetros ( D::D(int x, int y): C(x), B(y) {} )? ¿Qué instancia de A se elegirá para formar parte de D ?
  2. ¿Qué sucede si uso la herencia no virtual para B , pero la virtual para C ? ¿Es suficiente para crear una sola instancia de A en D ?
  3. ¿Debería siempre usar herencia virtual de forma predeterminada a partir de ahora como medida preventiva, ya que resuelve posibles problemas de diamantes con un menor costo de rendimiento y sin otros inconvenientes?

No poder predecir el comportamiento sin probar muestras de código significa no entender el concepto. Debajo está lo que me ayudó a entender la herencia virtual.

Doble a

Primero, comencemos con este código sin herencia virtual:

 #include 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 << "dA::getX() = " << dA::getX() << endl; cout << "dB::getX() = " << dB::getX() << endl; cout << "dC::getX() = " << dC::getX() << endl; } 

Vamos a ir a través de salida. Ejecutando 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 tanto B como C , cada uno de ellos creando su propia A , entonces 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 por la cual d.getX() causa un error de comstackción ya que el comstackdor no puede elegir para qué instancia A debe llamar el método. Todavía es posible llamar a los métodos directamente para la clase principal elegida:

 dB::getX() = 3 dC::getX() = 2 

Virtualidad

Ahora permitamos agregar herencia virtual. Usar la misma muestra 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 << "dA::getX() = " << dA::getX() << endl; //uncommented ... 

Vamos a saltar a la creación de d :

 Create d(2,3): A::A() C::C(2) B::B(3) D::D(2, 3) 

Puedes ver que A se crea con el constructor predeterminado ignorando los parámetros pasados ​​por los constructores de B y C Al desaparecer la ambigüedad, todas las llamadas a getX() devuelven el mismo valor:

 d.getX() = 42 dA::getX() = 42 dB::getX() = 42 dC::getX() = 42 

Pero, ¿y si queremos llamar al constructor parametrizado para A ? Se puede hacer llamando 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 constructores de padres directos solamente, pero hay una exclusión para el caso de herencia virtual. El descubrimiento de esta regla "hizo clic" para mí y me ayudó a entender mucho las interfaces virtuales:

class B: virtual A código class B: virtual A significa que cualquier clase heredada de B ahora es responsable de crear A por sí mismo, ya que B no lo hará automáticamente.

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

  1. Durante la creación de D ni B ni C son responsables de los parámetros de A , es totalmente solo hasta D
  2. C delegará la creación de A a D , pero B creará su propia instancia de A , devolviendo así el problema de los diamantes.
  3. Definir los parámetros de la clase base en la clase de nieto en lugar de dirigir al niño no es una buena práctica, por lo que debe tolerarse cuando existe un problema con el diamante y esta medida es inevitable.

El ejemplo de código correcto está aquí. El problema del diamante:

 #include  // Here you have the diamond problem : there is B::eat() and C::eat() // because they both inherit from A and contain independent copies of A::eat() // So what is D::eat()? Is it B::eat() or C::eat() ? class A { public: void eat(){ std::cout << "CHROME-CHROME" << endl; } }; class B: public A { }; class C: public A { }; class D: public B,C { }; int main(int argc, char ** argv){ A *a = new D(); a->eat(); delete a; } 

La solución :

 #include  // Virtual inheritance to ensure B::eat() and C::eat() to be the same class A { public: void eat(){ std::cout<< "CHROME-CHROME" << endl; } }; class B: virtual public A { }; class C: virtual public A { }; class D: public B,C { }; int main(int argc, char ** argv){ A *a = new D(); a->eat(); delete a; } 

Este problema puede resolverse utilizando palabras clave virtuales.

  A / \ BC \ / D 

Ejemplo de problema de diamantes.

 #include using namespace std; class AA { public: int a; AA() { a=10; } }; class BB: virtual public AA { public: int b; BB() { b=20; } }; class CC:virtual public AA { public: int c; CC() { c=30; } }; class DD:public BB,CC { public: int d; DD() { d=40; printf("Value of A=%d\n",a); } }; int main() { DD dobj; return 0; }