C ++ vtables. Parte 2 (herencia virtual + código generado por compilador)

La traducción del artículo fue preparada especialmente para estudiantes del curso "Desarrollador C ++" . ¿Es interesante desarrollar en esta dirección? ¡Mira la grabación de la clase práctica de Google Test Framework !



Parte 3 - Herencia virtual


En la primera y segunda parte de este artículo, hablamos sobre cómo funcionan las vtables en los casos más simples y luego en la herencia múltiple. La herencia virtual complica aún más la situación.


Como recordarán, la herencia virtual significa que en una clase particular solo hay una instancia de la clase base. Por ejemplo:


class ios ... class istream : virtual public ios ... class ostream : virtual public ios ... class iostream : public istream, public ostream 

Si no fuera por la palabra clave virtual anterior, iostream realmente tendría dos instancias de ios que podrían causar dolores de cabeza durante la sincronización y simplemente serían ineficaces.


Para comprender la herencia virtual, consideraremos el siguiente fragmento de código:


 #include <iostream> using namespace std; class Grandparent { public: virtual void grandparent_foo() {} int grandparent_data; }; class Parent1 : virtual public Grandparent { public: virtual void parent1_foo() {} int parent1_data; }; class Parent2 : virtual public Grandparent { public: virtual void parent2_foo() {} int parent2_data; }; class Child : public Parent1, public Parent2 { public: virtual void child_foo() {} int child_data; }; int main() { Child child; } 

Vamos a explorar child . Comenzaré volcando una gran cantidad de memoria exactamente donde comienza vtable Child , como hicimos en las partes anteriores, y luego analizaré los resultados. Sugiero echar un vistazo rápido al resultado aquí y volver a él cuando revele los detalles a continuación.


 (gdb) p child $1 = {<Parent1> = {<Grandparent> = {_vptr$Grandparent = 0x400998 <vtable for Child+96>, grandparent_data = 0}, _vptr$Parent1 = 0x400950 <vtable for Child+24>, parent1_data = 0}, <Parent2> = {_vptr$Parent2 = 0x400978 <vtable for Child+64>, parent2_data = 4195888}, child_data = 0} (gdb) x/600xb 0x400938 0x400938 <vtable for Child>: 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400940 <vtable for Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400948 <vtable for Child+16>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400950 <vtable for Child+24>: 0x70 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400958 <vtable for Child+32>: 0xa0 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400960 <vtable for Child+40>: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400968 <vtable for Child+48>: 0xf0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400970 <vtable for Child+56>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400978 <vtable for Child+64>: 0x90 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400980 <vtable for Child+72>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400988 <vtable for Child+80>: 0xe0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400990 <vtable for Child+88>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400998 <vtable for Child+96>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x4009a0 <VTT for Child>: 0x50 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009a8 <VTT for Child+8>: 0xf8 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009b0 <VTT for Child+16>: 0x18 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009b8 <VTT for Child+24>: 0x98 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009c0 <VTT for Child+32>: 0xb8 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009c8 <VTT for Child+40>: 0x98 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009d0 <VTT for Child+48>: 0x78 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009d8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009e0 <construction vtable for Parent1-in-Child>: 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009e8 <construction vtable for Parent1-in-Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009f0 <construction vtable for Parent1-in-Child+16>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009f8 <construction vtable for Parent1-in-Child+24>: 0x70 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400a00 <construction vtable for Parent1-in-Child+32>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a08 <construction vtable for Parent1-in-Child+40>: 0xe0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400a10 <construction vtable for Parent1-in-Child+48>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a18 <construction vtable for Parent1-in-Child+56>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400a20 <typeinfo name for Parent1>: 0x37 0x50 0x61 0x72 0x65 0x6e 0x74 0x31 0x400a28 <typeinfo name for Parent1+8>: 0x00 0x31 0x31 0x47 0x72 0x61 0x6e 0x64 0x400a30 <typeinfo name for Grandparent+7>: 0x70 0x61 0x72 0x65 0x6e 0x74 0x00 0x00 0x400a38 <typeinfo for Grandparent>: 0x50 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400a40 <typeinfo for Grandparent+8>: 0x29 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a48: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a50 <typeinfo for Parent1>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400a58 <typeinfo for Parent1+8>: 0x20 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a60 <typeinfo for Parent1+16>: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x400a68 <typeinfo for Parent1+24>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a70 <typeinfo for Parent1+32>: 0x03 0xe8 0xff 0xff 0xff 0xff 0xff 0xff 0x400a78: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a80 <construction vtable for Parent2-in-Child>: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a88 <construction vtable for Parent2-in-Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a90 <construction vtable for Parent2-in-Child+16>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a98 <construction vtable for Parent2-in-Child+24>: 0x90 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400aa0 <construction vtable for Parent2-in-Child+32>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400aa8 <construction vtable for Parent2-in-Child+40>: 0xf0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400ab0 <construction vtable for Parent2-in-Child+48>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400ab8 <construction vtable for Parent2-in-Child+56>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400ac0 <typeinfo name for Parent2>: 0x37 0x50 0x61 0x72 0x65 0x6e 0x74 0x32 0x400ac8 <typeinfo name for Parent2+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400ad0 <typeinfo for Parent2>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400ad8 <typeinfo for Parent2+8>: 0xc0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400ae0 <typeinfo for Parent2+16>: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x400ae8 <typeinfo for Parent2+24>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400af0 <typeinfo for Parent2+32>: 0x03 0xe8 0xff 0xff 0xff 0xff 0xff 0xff 0x400af8 <typeinfo name for Child>: 0x35 0x43 0x68 0x69 0x6c 0x64 0x00 0x00 0x400b00 <typeinfo for Child>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400b08 <typeinfo for Child+8>: 0xf8 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b10 <typeinfo for Child+16>: 0x02 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x400b18 <typeinfo for Child+24>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b20 <typeinfo for Child+32>: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b28 <typeinfo for Child+40>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b30 <typeinfo for Child+48>: 0x02 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x400b38 <vtable for Grandparent>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b40 <vtable for Grandparent+8>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b48 <vtable for Grandparent+16>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 

Wow, hay mucha información. Dos nuevas preguntas aparecen de inmediato: ¿qué es VTT y qué es la construcción vtable for X-in-Child ? Les responderemos lo suficientemente pronto.
Comencemos con la estructura de memoria secundaria:


TamañoValor
8 bytes_vptr $ Parent1
4 bytesparent1_data (+ 4 bytes de relleno)
8 bytes_vptr $ Parent2
4 bytesparent2_data
4 bytesdatos_niños
8 bytes_vptr $ Abuelo
4 bytesgrandparent_data (+ 4 bytes de relleno)

De hecho, Child tiene solo 1 instancia de abuelo. Lo no trivial es que él es el último en memoria, aunque es el más alto en la jerarquía.
Aquí está la estructura vtable :


La direccionValorContenido
0x4009380x20 (32)desplazamiento de base virtual (discutiremos esto pronto)
0x4009400 0top_offset
0x4009480x400b00typeinfo para niño
0x4009500x400870Parent1 :: parent1_foo (). El puntero vtable Parent1 apunta aquí.
0x4009580x4008a0Niño :: child_foo ()
0x4009600x10 (16)desplazamiento de base virtual
0x400968-16top_offset
0x40090x400btypeinfo para niño
7000
0x4009780x400890Parent2 :: parent2_foo (). El puntero vtable Parent2 apunta aquí.
0x4009800 0desplazamiento de base virtual
0x400988-32top_offset
0x4009900x400b00typeinfo para niño
0x4009980x400880Abuelo :: grandparent_foo (). El puntero vtable Abuelo señala aquí.

Arriba hay un nuevo concepto: virtual-base offset . Pronto entenderemos lo que está haciendo allí.
A continuación, exploremos estas construction vtables aspecto extraño. Aquí está la vtable for Parent1-in-Child construcción vtable for Parent1-in-Child :


ValorContenido
0x20 (32)desplazamiento de base virtual
0 0desplazamiento superior
0x400a50typeinfo para Parent1
0x400870Parent1 :: parent1_foo ()
0 0desplazamiento de base virtual
-32desplazamiento superior
0x400a50typeinfo para Parent1
0x400880Grandparent :: grandparent_foo ()

Por el momento, creo que sería más comprensible describir el proceso que apilar más tablas con números aleatorios. Entonces


Imagina que eres un Child . Se le pide que se construya en un nuevo recuerdo. Dado que hereda Grandparent directamente (que es lo que significa herencia virtual), llamará directamente a su constructor directamente (si no fuera herencia virtual, llamaría al constructor Parent1 , que a su vez llamaría al constructor Grandparent ). Establece this += 32 bytes, ya que aquí es donde se encuentran los datos de los Grandparent , y llama al constructor. Muy simple


Entonces es hora de construir Parent1 . Parent1 puede asumir con seguridad que para cuando se construya, Grandparent ya se haya creado, por lo que puede, por ejemplo, obtener acceso a los datos y métodos de Grandparent . Pero espera, ¿cómo puede saber dónde encontrar estos datos? ¡No están en el mismo lugar con las variables Parent1 !


La construction table for Parent1-in-Child entra en escena. Esta tabla es para decirle a Parent1 dónde encontrar datos a los que pueda acceder. this apunta a los datos de Parent1 . virtual-base offset indica dónde puede encontrar los datos de los abuelos: avance 32 bytes a partir de esto y encontrará la memoria de los Grandparent . Lo entiendes? La compensación de base virtual es similar a la de top_offset, pero para las clases virtuales.


Ahora que entendemos esto, la construcción de Parent2 es básicamente la misma, solo que usando la construction table for Parent2-in-Child . De hecho, Parent2-in-Child tiene un virtual-base offset de virtual-base offset de 16 bytes.


Deja que la información se empape un poco. ¿Estás listo para continuar? Bueno
Ahora volvamos a VTT . Aquí está la estructura VTT :


La direccionValorSímboloContenido
0x4009a00x400950vtable para niños + 24Entradas Parent1 en vtable Child
0x4009a80x4009f8construcción vtable para Parent1-in-Child + 24Métodos Parent1 en Parent1-in-Child
0x4009b00x400a18construcción vtable para Parent1-in-Child + 56Métodos de abuelos para padres1 en el niño
0x4009b80x400a98construcción vtable para Parent2-in-Child + 24Métodos Parent2 en Parent2-in-Child
0x4009c00x400ab8vtable de construcción para Parent2-in-Child + 56`Métodos abuelos para Parent2-in-Child
0x4009c80x400998vtable para niños + 96`Entradas de abuelos en vtable Child
0x4009d00x400978vtable para Niño + 64`Parent2 entradas en vtable Child

VTT significa virtual-table table , lo que significa que es una vtable. Esta es una tabla de traducción que sabe, por ejemplo, si Parent1 al constructor Parent1 para un objeto individual, para el objeto Parent1-in-Child o para Parent1-in-SomeOtherObject . Siempre aparece inmediatamente después de vtable , para que el compilador sepa dónde encontrarlo. Por lo tanto, no es necesario almacenar otro puntero en los objetos mismos.


Fuh ... muchos detalles, pero creo que cubrimos todo lo que quería cubrir. En la cuarta parte hablaremos sobre los detalles de las vtables nivel vtables . ¡No te saltes, ya que esta es probablemente la parte más importante de este artículo!


Parte 4 - Código generado por el compilador


En este punto de este artículo, aprendimos cómo las typeinfo vtables y typeinfo ajustan a nuestros binarios y cómo los usa el compilador. Ahora entenderemos la parte del trabajo que el compilador hace por nosotros automáticamente.


Constructores


Para el constructor de cualquier clase, se genera el siguiente código:


  • Llamar construcciones primarias, si las hay;
  • Establecer punteros vtable, si los hay;
  • Inicialización de miembros de acuerdo con la lista de inicializadores;
  • Ejecución de código dentro de paréntesis de constructor.

Todo lo anterior puede suceder sin un código explícito:


  • Los constructores principales se inician automáticamente de manera predeterminada a menos que se especifique lo contrario;
  • Los miembros se inicializan de manera predeterminada si no tienen un valor predeterminado o entradas en la lista de inicializadores;
  • Todo el constructor se puede marcar = predeterminado;
  • Solo la asignación de vtable siempre está oculta.

Aquí hay un ejemplo:


 #include <iostream> #include <string> using namespace std; class Parent { public: Parent() { Foo(); } virtual ~Parent() = default; virtual void Foo() { cout << "Parent" << endl; } int i = 0; }; class Child : public Parent { public: Child() : j(1) { Foo(); } void Foo() override { cout << "Child" << endl; } int j; }; class Grandchild : public Child { public: Grandchild() { Foo(); s = "hello"; } void Foo() override { cout << "Grandchild" << endl; } string s; }; int main() { Grandchild g; } 

Escribamos un pseudocódigo para el constructor de cada clase:


PadreNiñoNieto
1. vtable = vtable padre;1. Llama al constructor predeterminado Parent;1. Llama al constructor predeterminado Child;
2. i = 0;2. vtable = vtable Niño;2. vtable = vtable Nieto;
3. Llamadas Foo ();3. j = 1;3., Llama al constructor predeterminado s;
4. Llamadas Foo ();4. Llamadas Foo ();
5. Llama al operador = para s;

Dado esto, no es sorprendente que en el contexto del constructor de la clase, vtable se refiera a la vtable de esta clase en sí, y no a su clase específica. Esto significa que las llamadas virtuales se resuelven como si no hubiera herederos disponibles. Por lo tanto, la conclusión


 Parent Child Grandchild 

¿Qué pasa con las funciones virtuales puras? Si no se implementan (sí, puede implementar funciones puramente virtuales, pero ¿por qué necesita esto?), Probablemente (y con suerte) irá directamente a segfault. Algunos compiladores descuidan el error, lo cual es genial.


Destructores


Como puede imaginar, los destructores se comportan de la misma manera que los constructores, solo en el orden inverso.


Aquí hay un ejercicio rápido para pensar: ¿por qué los destructores cambian el puntero vtable para que apunte a su propia clase, en lugar de dejar un puntero a una clase específica? Respuesta: desde el momento en que se lanzó el destructor, cualquier clase heredada ya estaba destruida. Llamar a métodos de esta clase no es lo que quieres hacer.


Reparto implícito


Como vimos en la segunda y tercera parte , un puntero a un objeto hijo no es necesariamente igual al puntero padre de la misma instancia (como en el caso de herencia múltiple).


Sin embargo, para usted (el desarrollador), no hay trabajo adicional para llamar a una función que recibe un puntero principal. Esto se debe a que el compilador lo cambia implícitamente cuando agrega punteros y referencias a clases principales.


Reparto dinámico (RTTI)


Los modelos dinámicos utilizan tablas typeinfo , que examinamos en la primera parte. Hacen esto en tiempo de ejecución, observando la entrada typeinfo un puntero antes de lo que apunta el puntero vtable , y usan la clase desde allí para verificar si la conversión es posible.


Esto explica el costo de dynamic_cast cuando se usa con frecuencia.


Punteros de método


Planeo escribir una publicación completa sobre punteros a métodos en el futuro. Antes de eso, me gustaría enfatizar que un puntero a un método que apunta a una función virtual en realidad llamará a un método anulado (a diferencia de los punteros a funciones que no son miembros).


 // TODO:  ,     

Comprueba tu mismo!


Ahora puede explicarse por qué el siguiente fragmento de código se comporta de la manera en que se comporta:


 #include <iostream> using namespace std; class FooInterface { public: virtual ~FooInterface() = default; virtual void Foo() = 0; }; class BarInterface { public: virtual ~BarInterface() = default; virtual void Bar() = 0; }; class Concrete : public FooInterface, public BarInterface { public: void Foo() override { cout << "Foo()" << endl; } void Bar() override { cout << "Bar()" << endl; } }; int main() { Concrete c; c.Foo(); c.Bar(); FooInterface* foo = &c; foo->Foo(); BarInterface* bar = (BarInterface*)(foo); bar->Bar(); //  "Foo()" - WTF? } 

Esto concluye mi artículo de cuatro partes. Espero que aprendas algo nuevo, como yo.

Source: https://habr.com/ru/post/480610/


All Articles