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

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

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

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

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

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

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

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

    Почему все звонки f на печать 33?


person JeB    schedule 22.07.2012    source источник
comment
Это домашнее задание или любопытство?   -  person templatetypedef    schedule 23.07.2012
comment
Собственно это экзамен. Но я уверен, что если я наконец пойму, как все работает в этом примере, я смогу понять все, что связано с множественным наследованием и виртуальным наследованием.   -  person JeB    schedule 23.07.2012
comment
Вы можете легко узнать начало каждого родительского подобъекта следующим образом: C foo; intptr_t offsetB1 = (intptr_t)(B1*)&foo - (intptr_t)&foo;, начало других баз может быть получено аналогично. Кроме того, вычисление sizeof всех классов должно дать вам еще одну хорошую подсказку.   -  person cmaster - reinstate monica    schedule 18.07.2014


Ответы (2)


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

Представьте, что вы получаете 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 во время выполнения.

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

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

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

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

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

person Kerrek SB    schedule 22.07.2012
comment
Спасибо за отличный ответ. Насколько я знаю, thunk - это часть виртуальной таблицы. Т.е. если вам не нужно смещение, чтобы получить объектную функцию, над которой работает, вам не нужен преобразователь. Если вам нужно смещение, то в соответствующем поле vtable есть указатель на thunk, который содержит смещение и указатель на фактическую функцию. Поэтому мне интересно узнать, как в моем примере выглядят vtables. Т.е. на какие из функций они указывают и на какие функции указывают переходы. - person JeB; 23.07.2012
comment
Кроме того, я очень удивлен, что все приведения (статические, динамические, переинтерпретируемые) превращают меня в одну конкретную функцию C :: f. Это очень странно. Не могли бы вы объяснить, как (в этом примере) работает каждый из них? Кроме того, я прочитал много статей об этом, и статья, на которую вы указали ссылку, была одной из первых статей, которые я прочитал. Мне все еще не помогает понять, что здесь происходит. - person JeB; 23.07.2012
comment
@ user1544364 все приведения () обращает меня к одной конкретной функции Нет. Эти приведения возвращают указатель на объект, а не на функцию. - person curiousguy; 26.07.2012
comment
@ user1544364 преобразователь, содержащий смещение и указатель на фактическую функцию. Нет. Преобразователь не содержит данных, преобразователь состоит из исполняемого кода. Преобразователь - это просто оптимизированная функция. - person curiousguy; 26.07.2012
comment
Ссылка на прекрасную статью не работает, но я нашел резервную копию: cs.nyu.edu/courses/fall16/CSCI-UA.0470-001/slides/ - person tuket; 09.05.2019

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

#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-указателя различны, это позволяет программе вывести правильные позиции всех виртуальных баз.

person cmaster - reinstate monica    schedule 17.07.2014