Виртуальные таблицы и макет памяти в нескольких виртуальных наследования

Рассмотрим следующую иерархию:

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; }
};
  • Что такое точный формат памяти экземпляра C? Сколько vptrs оно содержит, где точно каждый из них помещен? Какая из виртуальных таблиц разделяется с виртуальной таблицей C? Что конкретно содержит каждая виртуальная таблица?

    Вот как я понимаю макет:

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

    где AptrOfBx - указатель на экземпляр A, который Bx содержит (так как наследование является виртуальным).
    Это верно? Какие функции vptr1 указывают на? Какие функции vptr2 указывают на?

  • Учитывая следующий код

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

    Почему все вызовы f печатают 33?

Ответ 1

Виртуальные базы сильно отличаются от обычных баз. Помните, что "виртуальный" означает "определено во время выполнения" - таким образом, весь базовый подобъект должен быть определен во время выполнения.

Представьте, что вы получаете ссылку B & x, и вам нужно найти член A::a. Если наследование было реальным, то B имеет суперкласс A, и, следовательно, объект B, который вы просматриваете через x, имеет A -подбор, в котором вы можете найти своего участника A::a, Если самый производный объект x имеет несколько оснований типа A, вы можете видеть только ту конкретную копию, которая является подобъектом B.

Но если наследование виртуально, ничто из этого не имеет смысла. Мы не знаем, какой A -подборъект нам нужен - эта информация просто не существует во время компиляции. Мы могли бы иметь дело с фактическим B -объектом, как в B y; B & x = y;, или с C -объектом, подобным C z; B & x = z;, или чем-то совершенно другим, который происходит практически из A еще много раз. Единственный способ узнать, найти фактическую базу A во время выполнения.

Это может быть реализовано с еще одним уровнем безотказной работы. (Обратите внимание, что это полностью совпадает с тем, как виртуальные функции реализованы с одним дополнительным уровнем времени выполнения по сравнению с не виртуальными функциями.) Вместо того, чтобы иметь указатель на виртуальный объект или базовый подобъект, одним из решений является сохранение указателя на указатель к фактическому базовому подобъекту. Это иногда называют "битком" или "батутом".

Таким образом, фактический объект C z; может выглядеть следующим образом. Фактическое упорядочение в памяти зависит от компилятора и несущественно, и я отключил vtables.

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

Таким образом, независимо от того, есть ли у вас B1& или B2&, вы сначала просматриваете thunk, и тот, в свою очередь, говорит вам, где найти фактический базовый подобъект. Это также объясняет, почему вы не можете выполнить статичное преобразование из A& в любой из производных типов: эта информация просто не существует во время компиляции.

Для более подробного объяснения посмотрите эту прекрасную статью. (В этом описании thunk является частью vtable C, а виртуальное наследование всегда требует обслуживания vtables, даже если виртуальных функций нет где угодно.)

Ответ 2

Я немного сжал ваш код следующим образом:

#include <stdio.h>
#include <stdint.h>

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");
}

Как вы видите, это печатает довольно много дополнительной информации, которая позволяет нам выводить макет памяти. Выход на моем компьютере (64-битный Linux, маленький порядковый порядок байтов) таков:

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 

Итак, мы можем описать макет следующим образом:

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

Здесь xx обозначает дополнение. Обратите внимание, как компилятор разместил переменную c в дополнении своей не виртуальной базы. Также обратите внимание, что все три v-указателя отличаются друг от друга, что позволяет программе выводить правильные положения всех виртуальных баз.