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 .B
C
A
D
B
C
D
A
B
C
En cambio, si las clases B
y C
heredan virtualmente de la clase A
, entonces los objetos de la clase D
contendrá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 ( D
en 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 ( B
o 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.
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.Eat
es ambigua porque hay dos Animal
clases base (indirectas) en Bat
, por lo que cualquier Bat
objeto tiene dos Animal
subobjetos de clase base diferentes. Por lo tanto, un intento de vincular directamente una referencia al Animal
subobjeto de un Bat
objeto 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 bat
a 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 Animal
es probablemente no deseada, ya que queremos modelar que la relación ( Bat
es un Animal
) existe solo una vez; que a Bat
sea un Mammal
y sea un WingedAnimal
, no implica que sea un Animal
dos veces: una Animal
clase base corresponde a un contrato que Bat
implementa (la relación " es un " anterior realmente significa " implementa los requisitos de "), y a Bat
solo implementa el Animal
contrato una vez. El significado del mundo real de " es un solo una vez" es que Bat
debería tener solo una forma de implementar Eat
, no dos formas diferentes, dependiendo de si la Mammal
vista de Bat
está comiendo, o la WingedAnimal
vista de Bat
. (En el primer ejemplo de código vemos que Eat
no se reemplaza ni en Mammal
ni en WingedAnimal
, por lo que los dos Animal
subobjetos 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.
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 Animal
parte de Bat::WingedAnimal
ahora es la misma Animal
instancia que la utilizada por Bat::Mammal
, es decir, a Bat
tiene solo una instancia compartida Animal
en su representación y, por lo tanto, una llamada a Bat::Eat
es inequívoca. Además, una conversión directa de Bat
to Animal
también es inequívoca, ahora que solo existe una Animal
instancia que Bat
podría convertirse a.
La capacidad de compartir una única instancia del Animal
padre entre Mammal
y WingedAnimal
se habilita al registrar el desplazamiento de memoria entre los miembros Mammal
or 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 .WingedAnimal
Animal
Bat
vpointer
Mammal
vpointer
WingedAnimal
Bat
Animal
Animal
Mammal
WingedAnimal
Animal
Bat
Bat
Animal
Mammal
Squirrel
Mammal
Squirrel
Mammal
Bat
Squirrel
Bat
Este ejemplo ilustra un caso en el que la clase base A
tiene una variable constructora msg
y un antecesor adicional E
se deriva de la clase nieta D
.
A / \ ANTES DE CRISTO \ / D | mi
Aquí, A
se debe construir tanto en D
como en E
. Además, la inspección de la variable msg
ilustra cómo la clase A
se convierte en una clase base directa de su clase derivada, en oposición a una clase base de cualquier clase derivada intermedia entre A
y 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 }
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 }
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:
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.