stringtranslate.com

Herencia virtual

Diagrama de la herencia de diamante , un problema que la herencia virtual intenta resolver.

La herencia virtual es una técnica de C++ que garantiza que las clases derivadas de los nietos hereden solo una copia de las variables miembro de una clase base . Sin herencia virtual, si dos clases y heredan de una clase , y una clase hereda de ambas y , entonces contendrá dos copias de las variables miembro de : una a través de , y otra a través de . Se podrá acceder a ellas de forma independiente, mediante la resolución de ámbito .BCADBCDABC

En cambio, si las clases By Cheredan virtualmente de la clase A, entonces los objetos de la clase Dcontendrán solo un conjunto de variables miembro de la clase A.

Esta característica es más útil para la herencia múltiple , ya que convierte a la base virtual en un subobjeto común para la clase derivada y todas las clases que se derivan de ella. Esto se puede utilizar para evitar el problema del diamante al aclarar la ambigüedad sobre qué clase antecesora utilizar, ya que desde la perspectiva de la clase derivada ( Den el ejemplo anterior) la base virtual ( A) actúa como si fuera la clase base directa de D, no una clase derivada indirectamente a través de una base ( Bo C). [1] [2]

Se utiliza cuando la herencia representa la restricción de un conjunto en lugar de la composición de partes. En C++, una clase base que se pretende que sea común en toda la jerarquía se denota como virtual con la virtual palabra clave .

Considere la siguiente jerarquía de clases.

Herencia virtual UML.svg

struct Animal { virtual ~ Animal () = default ; // Muestra explícitamente que se creará el destructor de clase predeterminado. virtual void Eat () {} };           estructura Mamífero : Animal { virtual void Respirar () {} };       estructura WingedAnimal : Animal { virtual void Flap () {} };       // Un murciélago es un mamífero alado struct Bat : Mammal , WingedAnimal {};    

Como se declaró anteriormente, una llamada a bat.Eates ambigua porque hay dos Animalclases base (indirectas) en Bat, por lo que cualquier Batobjeto tiene dos Animalsubobjetos de clase base diferentes. Por lo tanto, un intento de vincular directamente una referencia al Animalsubobjeto de un Batobjeto fallaría, ya que la vinculación es inherentemente ambigua:

Murciélago murciélago ; Animal y animal = murciélago ; // error: ¿en qué subobjeto Animal debería convertirse un Murciélago, un Mamífero::Animal o un AnimalAlado::Animal?      

Para eliminar la ambigüedad, habría que convertir explícitamente bata cualquiera de los subobjetos de la clase base:

Murciélago murciélago ; Animal & mamífero = static_cast < Mamífero &> ( murciélago ); Animal & alado = static_cast < AnimalAlado &> ( murciélago );        

Para llamar a Eat, se necesita la misma desambiguación o calificación explícita: static_cast<Mammal&>(bat).Eat()o static_cast<WingedAnimal&>(bat).Eat()o alternativamente bat.Mammal::Eat()y bat.WingedAnimal::Eat(). La calificación explícita no solo utiliza una sintaxis más sencilla y uniforme tanto para punteros como para objetos, sino que también permite el envío estático, por lo que podría decirse que sería el método preferible.

En este caso, la doble herencia de Animales probablemente no deseada, ya que queremos modelar que la relación ( Bates un Animal) existe solo una vez; que a Batsea un Mammaly sea un WingedAnimal, no implica que sea un Animaldos veces: una Animalclase base corresponde a un contrato que Batimplementa (la relación " es un " anterior realmente significa " implementa los requisitos de "), y a Batsolo implementa el Animalcontrato una vez. El significado del mundo real de " es un solo una vez" es que Batdebería tener solo una forma de implementar Eat, no dos formas diferentes, dependiendo de si la Mammalvista de Batestá comiendo, o la WingedAnimalvista de Bat. (En el primer ejemplo de código vemos que Eatno se reemplaza ni en Mammalni en WingedAnimal, por lo que los dos Animalsubobjetos en realidad se comportarán de la misma manera, pero este es solo un caso degenerado, y eso no hace una diferencia desde el punto de vista de C++).

Esta situación se conoce a veces como herencia de diamante (véase Problema del diamante ) porque el diagrama de herencia tiene forma de diamante. La herencia virtual puede ayudar a resolver este problema.

La solución

Podemos volver a declarar nuestras clases de la siguiente manera:

estructura Animal { virtual ~ Animal () = predeterminado ; virtual void Eat () {} };          // Dos clases que heredan virtualmente Animal: struct Mammal : virtual Animal { virtual void Breathe () {} };        estructura WingedAnimal : virtual Animal { virtual void Flap () {} };        // Un murciélago sigue siendo un mamífero alado struct Bat : Mammal , WingedAnimal {};    

La Animalparte de Bat::WingedAnimalahora es la misma Animal instancia que la utilizada por Bat::Mammal, es decir, a Battiene solo una instancia compartida Animalen su representación y, por lo tanto, una llamada a Bat::Eates inequívoca. Además, una conversión directa de Batto Animaltambién es inequívoca, ahora que solo existe una Animalinstancia que Batpodría convertirse a.

La capacidad de compartir una única instancia del Animalpadre entre Mammaly WingedAnimalse habilita al registrar el desplazamiento de memoria entre los miembros Mammalor y los de la base dentro de la clase derivada. Sin embargo, este desplazamiento solo se puede conocer en el caso general en tiempo de ejecución, por lo que debe convertirse en ( , , , , , ). Hay dos punteros vtable , uno por jerarquía de herencia que hereda virtualmente . En este ejemplo, uno para y otro para . Por lo tanto, el tamaño del objeto ha aumentado en dos punteros, pero ahora solo hay uno y no hay ambigüedad. Todos los objetos de tipo usarán los mismos punteros v, pero cada objeto contendrá su propio objeto único. Si otra clase hereda de , como , entonces el puntero v en la parte de generalmente será diferente al puntero v en la parte de aunque pueden ser iguales si la clase tiene el mismo tamaño que .WingedAnimalAnimalBatvpointerMammalvpointerWingedAnimalBatAnimalAnimalMammalWingedAnimalAnimalBatBatAnimalMammalSquirrelMammalSquirrelMammalBatSquirrelBat

Ejemplo adicional de varios antepasados

Este ejemplo ilustra un caso en el que la clase base Atiene una variable constructora msgy un antecesor adicional Ese deriva de la clase nieta D.

 A  / \ ANTES DE CRISTO  \ /  D | mi

Aquí, Ase debe construir tanto en Dcomo en E. Además, la inspección de la variable msgilustra cómo la clase Ase convierte en una clase base directa de su clase derivada, en oposición a una clase base de cualquier clase derivada intermedia entre Ay la clase derivada final. El código a continuación se puede explorar de forma interactiva aquí.

#include <cadena> #include <iostream>  clase A { privado : std :: string _msg ; público : A ( std :: string x ) : _msg ( x ) {} void prueba (){ std :: cout << "hola desde A: " << _msg << " \n " ; } };                    // B,C heredan A virtualmente clase B : virtual public A { public : B ( std :: string x ) : A ( "b" ){} }; clase C : virtual public A { public : C ( std :: string x ) : A ( "c" ){} }; // Error de compilación cuando se elimina :A("c") (ya que no se llama al constructor de A) //clase C: virtual public A { public: C(std::string x){} }; //clase C: virtual public A { public: C(std::string x){ A("c"); } }; // Mismo error de compilación                  // Dado que B y C heredan A virtualmente, A debe construirse en cada clase hija D : public B , C { public : D ( std :: string x ) : A ( "d_a" ), B ( "d_b" ), C ( "d_c" ){} }; clase E : public D { public : E ( std :: string x ) : A ( "e_a" ), D ( "e_d" ){} };                  // Error de compilación sin construir A //class D: public B,C { public: D(std::string x):B(x),C(x){} };// Error de compilación sin construir A //clase E: public D { public: E(std::string x):D(x){} };int main ( int argc , char ** argv ){ D d ( "d" ); d . test (); // hola desde A: d_a           E e ( "e" ); e . test (); // hola desde A: e_a }    

Métodos virtuales puros

Supongamos que se define un método virtual puro en la clase base. Si una clase derivada hereda virtualmente la clase base, no es necesario definir el método virtual puro en esa clase derivada. Sin embargo, si la clase derivada no hereda virtualmente la clase base, se deben definir todos los métodos virtuales. El código siguiente se puede explorar de forma interactiva aquí.

#include <cadena> #include <iostream>  clase A { protegido : std :: string _msg ; público : A ( std :: string x ) : _msg ( x ) {} void prueba (){ std :: cout << "hola desde A: " << _msg << " \n " ; } void virtual prueba_virtual_pura () = 0 ; };                          // dado que B, C heredan A virtualmente, no es necesario definir el método virtual puro pure_virtual_test clase B : virtual pública A { pública : B ( std :: string x ) : A ( "b" ){} }; clase C : virtual pública A { pública : C ( std :: string x ) : A ( "c" ){} };                    // dado que B, C heredan A virtualmente, A debe construirse en cada hijo // sin embargo, dado que D no hereda B, C virtualmente, el método virtual puro en A *debe definirse* class D : public B , C { public : D ( std :: string x ) : A ( "d_a" ), B ( "d_b" ), C ( "d_c" ){} void pure_virtual_test () override { std :: cout << "pure virtual hello from: " << _msg << " \n " ; } };                 // no es necesario redefinir el método virtual puro después de que el padre lo define clase E : public D { public : E ( std :: string x ) : A ( "e_a" ), D ( "e_d" ){} };          int main ( int argc , char ** argv ){ D d ( "d" ); d . test (); // hola desde A: d_a d . pure_virtual_test (); // hola virtual puro desde: d_a            E e ( "e" ); e . test (); // hola desde A: e_a e . pure_virtual_test (); // hola virtual puro desde: e_a }      

Referencias

  1. ^ Milea, Andrei. "Resolver el problema del diamante con herencia virtual". Cprogramming.com . Consultado el 8 de marzo de 2010. Uno de los problemas que surgen debido a la herencia múltiple es el problema del diamante. Bjarne Stroustrup (el creador de C++) ofrece una ilustración clásica de esto en el siguiente ejemplo:
  2. ^ McArdell, Ralph (14 de febrero de 2004). "C++/¿Qué es la herencia virtual?". Todos los expertos . Archivado desde el original el 10 de enero de 2010. Consultado el 8 de marzo de 2010. Esto es algo que puede resultar necesario si se utiliza herencia múltiple. En ese caso , es posible que una clase se derive de otras clases que tengan la misma clase base. En tales casos, sin herencia virtual, los objetos contendrán más de un subobjeto del tipo base que comparten las clases base. Si este es el efecto requerido depende de las circunstancias. Si no lo es, puede utilizar la herencia virtual especificando clases base virtuales para aquellos tipos base para los que un objeto completo solo debe contener un subobjeto de clase base.