Виртуальные таблицы и макет памяти в нескольких виртуальных наследования
Рассмотрим следующую иерархию:
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-указателя отличаются друг от друга, что позволяет программе выводить правильные положения всех виртуальных баз.