Tablas virtuales y diseño de memoria en múltiples herencia virtual

Considera seguir jerarquía:

struct A { int a; A() { f(0); } A(int i) { f(i); } virtual void f(int i) { cout << i; } }; struct B1 : virtual A { int b1; B1(int i) : A(i) { f(i); } virtual void f(int i) { cout << i+10; } }; struct B2 : virtual A { int b2; B2(int i) : A(i) { f(i); } virtual void f(int i) { cout << i+20; } }; struct C : B1, virtual B2 { int c; C() : B1(6),B2(3),A(1){} virtual void f(int i) { cout << i+30; } }; 
  1. ¿Cuál es el diseño de memoria exacto de la instancia C ? ¿Cuántos vptrs contiene, dónde exactamente se coloca cada uno de ellos? ¿Cuál de las tablas virtuales se comparte con la tabla virtual de C? ¿Qué contiene exactamente cada tabla virtual?

    Aquí cómo entiendo el diseño:

     ---------------------------------------------------------------- |vptr1 | AptrOfB1 | b1 | B2ptr | c | vptr2 | AptrOfB2 | b2 | a | ---------------------------------------------------------------- 

    donde AptrOfBx es el puntero a A instancia que contiene Bx (ya que la herencia es virtual).
    ¿Es eso correcto? ¿A qué funciones apunta vptr1 ? ¿A qué funciones apunta vptr2 ?

  2. Dado el siguiente código

     C* c = new C(); dynamic_cast(c)->f(3); static_cast(c)->f(3); reinterpret_cast(c)->f(3); 

    ¿Por qué todas las llamadas a print 33 ?

Las bases virtuales son muy diferentes de las bases ordinarias. Recuerde que “virtual” significa “determinado en tiempo de ejecución”, por lo que todo el subobjeto base debe determinarse en tiempo de ejecución.

Imagine que obtiene una referencia de B & x , y tiene la tarea de encontrar al miembro A::a . Si la herencia es real, entonces B tiene una superclase A , y por lo tanto el objeto B que estás viendo a través de x tiene un sub-objeto A en el que puedes ubicar a tu miembro A::a . Si el objeto más derivado de x tiene múltiples bases de tipo A , entonces solo puede ver esa copia particular que es el subobjeto de B

Pero si la herencia es virtual, nada de esto tiene sentido. No sabemos qué A -subobject necesitamos; esta información simplemente no existe en el momento de la comstackción. Podríamos tratar con un objeto B real como en B y; B & x = y; B y; B & x = y; , o con un objeto C como C z; B & x = z; C z; B & x = z; , o algo completamente diferente que se deriva virtualmente de A muchas más veces. La única forma de saber es encontrar la base real A en tiempo de ejecución .

Esto se puede implementar con un nivel más de direccionamiento en tiempo de ejecución. (Tenga en cuenta que esto es totalmente paralelo a cómo se implementan las funciones virtuales con un nivel adicional de direccionamiento en tiempo de ejecución en comparación con las funciones no virtuales.) En lugar de tener un puntero a un subobjeto vtable o base, una solución es almacenar un puntero a un puntero al subobjeto base real. Esto a veces se llama “thunk” o “trampoline”.

Entonces el objeto real C z; puede verse de la siguiente manera. El orden real en la memoria depende del comstackdor y no es importante, y he suprimido los vtables.

 +-+------++-+------++-----++-----+ |T| B1 ||T| B2 || C || A | +-+------++-+------++-----++-----+ | | | VV ^ | | +-Thunk-+ | +--->>----+-->>---| ->>-+ +-------+ 

Por lo tanto, no importa si tiene un B1& un B2& , primero busca el procesador y ese a su vez le indica dónde encontrar el subobjeto base real. Esto también explica por qué no puede realizar un envío estático desde un A& a cualquiera de los tipos derivados: esta información simplemente no existe en tiempo de comstackción.

Para una explicación más detallada, eche un vistazo a este artículo fino . (En esa descripción, el thunk es parte del vtable de C , y la herencia virtual siempre requiere el mantenimiento de vtables, incluso si no hay funciones virtuales en ninguna parte).

He sombreado tu código un poco de la siguiente manera:

 #include  #include  struct A { int a; A() : a(32) { f(0); } A(int i) : a(32) { f(i); } virtual void f(int i) { printf("%d\n", i); } }; struct B1 : virtual A { int b1; B1(int i) : A(i), b1(33) { f(i); } virtual void f(int i) { printf("%d\n", i+10); } }; struct B2 : virtual A { int b2; B2(int i) : A(i), b2(34) { f(i); } virtual void f(int i) { printf("%d\n", i+20); } }; struct C : B1, virtual B2 { int c; C() : B1(6),B2(3),A(1), c(35) {} virtual void f(int i) { printf("%d\n", i+30); } }; int main() { C foo; intptr_t address = (intptr_t)&foo; printf("offset A = %ld, sizeof A = %ld\n", (intptr_t)(A*)&foo - address, sizeof(A)); printf("offset B1 = %ld, sizeof B1 = %ld\n", (intptr_t)(B1*)&foo - address, sizeof(B1)); printf("offset B2 = %ld, sizeof B2 = %ld\n", (intptr_t)(B2*)&foo - address, sizeof(B2)); printf("offset C = %ld, sizeof C = %ld\n", (intptr_t)(C*)&foo - address, sizeof(C)); unsigned char* data = (unsigned char*)address; for(int offset = 0; offset < sizeof(C); offset++) { if(!(offset & 7)) printf("| "); printf("%02x ", (int)data[offset]); } printf("\n"); } 

Como ve, esto imprime bastante información adicional que nos permite deducir el diseño de la memoria. La salida en mi máquina (un orden de linux de 64 bits, little endian) es esta:

 1 23 16 offset A = 16, sizeof A = 16 offset B1 = 0, sizeof B1 = 32 offset B2 = 32, sizeof B2 = 32 offset C = 0, sizeof C = 48 | 00 0d 40 00 00 00 00 00 | 21 00 00 00 23 00 00 00 | 20 0d 40 00 00 00 00 00 | 20 00 00 00 00 00 00 00 | 48 0d 40 00 00 00 00 00 | 22 00 00 00 00 00 00 00 

Entonces, podemos describir el diseño de la siguiente manera:

 +--------+----+----+--------+----+----+--------+----+----+ | vptr | b1 | c | vptr | a | xx | vptr | b2 | xx | +--------+----+----+--------+----+----+--------+----+----+ 

Aquí, xx denota el relleno. Observe cómo el comstackdor ha colocado la variable c en el relleno de su base no virtual. Tenga en cuenta también que los tres v-punteros son diferentes, esto permite que el progtwig deduzca las posiciones correctas de todas las bases virtuales.