Ответ 1
Твой вопрос интересен, однако я боюсь, что ты слишком большой, как первый вопрос, поэтому я отвечу в несколько шагов, если ты не против:)
Отказ от ответственности: я не автор-компилятор, и хотя я, конечно же, изучил этот вопрос, мое слово следует принимать с осторожностью. Там будут неточности. И я не так хорошо разбираюсь в RTTI. Кроме того, поскольку это не является стандартным, я описываю возможности.
1. Как реализовать наследование?
Примечание. Я оставлю проблемы с выравниванием, они просто означают, что некоторые блокировки могут быть включены между блоками
Теперь оставьте это виртуальными методами и сосредоточьтесь на том, как реализовано наследование, ниже.
Истина заключается в том, что наследование и состав разделяют много:
struct B { int t; int u; };
struct C { B b; int v; int w; };
struct D: B { int v; int w; };
Будут выглядеть:
B:
+-----+-----+
| t | u |
+-----+-----+
C:
+-----+-----+-----+-----+
| B | v | w |
+-----+-----+-----+-----+
D:
+-----+-----+-----+-----+
| B | v | w |
+-----+-----+-----+-----+
Удивительно, что это не так:)?
Это означает, однако, что множественное наследование довольно просто понять:
struct A { int r; int s; };
struct M: A, B { int v; int w; };
M:
+-----+-----+-----+-----+-----+-----+
| A | B | v | w |
+-----+-----+-----+-----+-----+-----+
Используя эти диаграммы, давайте посмотрим, что происходит при наведении производного указателя на базовый указатель:
M* pm = new M();
A* pa = pm; // points to the A subpart of M
B* pb = pm; // points to the B subpart of M
Используя нашу предыдущую диаграмму:
M:
+-----+-----+-----+-----+-----+-----+
| A | B | v | w |
+-----+-----+-----+-----+-----+-----+
^ ^
pm pb
pa
Тот факт, что адрес pb
немного отличается от адреса pm
, обрабатывается автоматически с помощью арифметики указателя для вас компилятором.
2. Как реализовать виртуальное наследование?
Виртуальное наследование сложно: вы должны убедиться, что один объект V
(для виртуального) будет совместно использоваться всеми другими подобъектами. Пусть определим простое наследование алмазов.
struct V { int t; };
struct B: virtual V { int u; };
struct C: virtual V { int v; };
struct D: B, C { int w; };
Я оставлю это представление и сконцентрируюсь на том, что в объекте D
оба подчасти B
и C
используют один и тот же подобъект. Как это можно сделать?
- Помните, что размер класса должен быть постоянным
- Помните, что при разработке ни B, ни C не могут предвидеть, будут ли они использоваться вместе или нет.
Решение, которое было найдено, поэтому прост: B
и C
зарезервировать место для указателя на V
и:
- если вы создадите автономный
B
, конструктор выделитV
в куче, который будет обрабатываться автоматически - если вы построите
B
как частьD
, подчастиB
ожидает, что конструкторD
передаст указатель на расположениеV
И idem для C
, очевидно.
В D
оптимизация позволяет конструктору зарезервировать пространство для V
прямо в объекте, потому что D
фактически не наследуется ни от B
, ни от C
, указав диаграмму, которую вы показали ( хотя у нас пока нет виртуальных методов).
B: (and C is similar)
+-----+-----+
| V* | u |
+-----+-----+
D:
+-----+-----+-----+-----+-----+-----+
| B | C | w | A |
+-----+-----+-----+-----+-----+-----+
Теперь замечаем, что кастинг от B
до A
немного сложнее, чем простая арифметика указателя: вам нужно следовать указателю в B
, а не простой арифметикой указателя.
Есть худший случай, хотя, кастинг. Если я дам вам указатель на A
, как вы узнаете, как вернуться к B
?
В этом случае магия выполняется dynamic_cast
, но для этого требуется некоторая поддержка (то есть информация), которая хранится где-то. Это так называемая RTTI
(информация о типе времени выполнения). dynamic_cast
сначала определит, что A
является частью D
через некоторую магию, затем запрашивает информацию о времени выполнения D, чтобы узнать, где в D
сохраняется подобъект B
.
Если бы мы были в случае, если не существует субобъекта B
, он либо вернет 0 (форму указателя), либо выбросит исключение bad_cast
(эталонная форма).
3. Как реализовать виртуальные методы?
В общем, виртуальные методы реализуются через v-таблицу (т.е. таблицу указателей на функции) для каждого класса и v-ptr для этой таблицы для каждого объекта. Это не единственная возможная реализация, и было продемонстрировано, что другие могут быть быстрее, однако это просто и с предсказуемыми издержками (как в плане памяти, так и скорости отправки).
Если мы возьмем простой объект базового класса с виртуальным методом:
struct B { virtual foo(); };
Для компьютера нет таких вещей, как методы-члены, поэтому на самом деле у вас есть:
struct B { VTable* vptr; };
void Bfoo(B* b);
struct BVTable { RTTI* rtti; void (*foo)(B*); };
Когда вы выходите из B
:
struct D: B { virtual foo(); virtual bar(); };
Теперь у вас есть два виртуальных метода: один переопределяет B::foo
, другой - новый. Компьютерное представление сродни:
struct D { VTable* vptr; }; // single table, even for two methods
void Dfoo(D* d); void Dbar(D* d);
struct DVTable { RTTI* rtti; void (*foo)(D*); void (*foo)(B*); };
Обратите внимание, что BVTable
и DVTable
настолько похожи (поскольку мы положили foo
до bar
)? Это важно!
D* d = /**/;
B* b = d; // noop, no needfor arithmetic
b->foo();
Переведите вызов foo
на машинный язык (несколько):
// 1. get the vptr
void* vptr = b; // noop, it stored at the first byte of B
// 2. get the pointer to foo function
void (*foo)(B*) = vptr[1]; // 0 is for RTTI
// 3. apply foo
(*foo)(b);
Эти vptrs инициализируются конструкторами объектов при выполнении конструктора D
, вот что произошло:
-
D::D()
вызываетB::B()
прежде всего, чтобы инициализировать свои подчасти -
B::B()
инициализируйтеvptr
, чтобы указать на его таблицу vtable, затем возвращает -
D::D()
инициализируйтеvptr
, чтобы указать на его таблицу vtable, переопределяя B
Следовательно, vptr
здесь указывает на D vtable, и, таким образом, применяемый foo
был D. Для B
он был полностью прозрачным.
Здесь B и D используют один и тот же vptr!
4. Виртуальные таблицы в многоуровневом
К сожалению, этот обмен не всегда возможен.
Во-первых, как мы видели, в случае виртуального наследования "общий" элемент задан странно в конечном завершенном объекте. Поэтому у него есть собственный vptr. Это 1.
Во-вторых, в случае многонаследования первая база выравнивается с полным объектом, но вторая база не может быть (им обоим нужно пространство для своих данных), поэтому она не может делиться своим vptr. Это 2.
В-третьих, первая база выровнена с полным объектом, тем самым предлагая нам тот же макет, что и в случае простого наследования (такая же возможность оптимизации). Это 3.
Довольно просто, нет?