Строительство / Разрушение в присутствии множественного наследования
КакВышеуказанный объект создается в памяти, когда создается сам объектИ как мы можем гарантировать, что частично построенный объект (и его vtable) безопасны для работы с конструкторами?
К счастью, все это обрабатывается очень тщательно для нас.Скажем, мы создаем новый объект типа D
(например, через new D
).Во-первых, память для объекта выделяется в куче и возвращается указатель.Вызывается конструктор D
, но перед выполнением какой-либо D
-специфической конструкции он вызывает конструктор A
объекта (после настройки указателя this
, конечно!).Конструктор A
заполняет часть A
объекта D
, как если бы он был экземпляром A
.
d --> +----------+
| |
+----------+
| |
+----------+
| |
+----------+
| | +-----------------------+
+----------+ | 0 (top_offset) |
| | +-----------------------+
+----------+ | ptr to typeinfo for A |
| vtable |-----> +-----------------------+
+----------+ | A::v() |
| a | +-----------------------+
+----------+
Управление возвращается конструктору D
,который вызывает конструктор B
.(Регулировка указателя здесь не требуется.) Когда конструктор B
сделан, объект выглядит следующим образом:
B-in-D
+-----------------------+
| 20 (vbase_offset) |
+-----------------------+
| 0 (top_offset) |
+-----------------------+
d --> +----------+ | ptr to typeinfo for B |
| vtable |------> +-----------------------+
+----------+ | B::w() |
| b | +-----------------------+
+----------+ | 0 (vbase_offset) |
| | +-----------------------+
+----------+ | -20 (top_offset) |
| | +-----------------------+
+----------+ | ptr to typeinfo for B |
| | +--> +-----------------------+
+----------+ | | A::v() |
| vtable |---+ +-----------------------+
+----------+
| a |
+----------+
Но подождите ... Конструктор B
изменил A
часть объекта путем изменения его указателя vtable!Как он узнал, что нужно отличать этот вид B-in-D от B-in-что-то еще (или, в этом отношении, автономного B
)?Просто. таблица виртуальных таблиц сказала это сделать.Эта структура, сокращенно VTT , представляет собой таблицу таблиц, используемых в строительстве.В нашем случае VTT для D
выглядит так:
B-in-D
+-----------------------+
| 20 (vbase_offset) |
VTT for D +-----------------------+
+-------------------+ | 0 (top_offset) |
| vtable for D |-------------+ +-----------------------+
+-------------------+ | | ptr to typeinfo for B |
| vtable for B-in-D |-------------|----------> +-----------------------+
+-------------------+ | | B::w() |
| vtable for B-in-D |-------------|--------+ +-----------------------+
+-------------------+ | | | 0 (vbase_offset) |
| vtable for C-in-D |-------------|-----+ | +-----------------------+
+-------------------+ | | | | -20 (top_offset) |
| vtable for C-in-D |-------------|--+ | | +-----------------------+
+-------------------+ | | | | | ptr to typeinfo for B |
| vtable for D |----------+ | | | +-> +-----------------------+
+-------------------+ | | | | | A::v() |
| vtable for D |-------+ | | | | +-----------------------+
+-------------------+ | | | | |
| | | | | C-in-D
| | | | | +-----------------------+
| | | | | | 12 (vbase_offset) |
| | | | | +-----------------------+
| | | | | | 0 (top_offset) |
| | | | | +-----------------------+
| | | | | | ptr to typeinfo for C |
| | | | +----> +-----------------------+
| | | | | C::x() |
| | | | +-----------------------+
| | | | | 0 (vbase_offset) |
| | | | +-----------------------+
| | | | | -12 (top_offset) |
| | | | +-----------------------+
| | | | | ptr to typeinfo for C |
| | | +-------> +-----------------------+
| | | | A::v() |
| | | +-----------------------+
| | |
| | | D
| | | +-----------------------+
| | | | 20 (vbase_offset) |
| | | +-----------------------+
| | | | 0 (top_offset) |
| | | +-----------------------+
| | | | ptr to typeinfo for D |
| | +----------> +-----------------------+
| | | B::w() |
| | +-----------------------+
| | | D::y() |
| | +-----------------------+
| | | 12 (vbase_offset) |
| | +-----------------------+
| | | -8 (top_offset) |
| | +-----------------------+
| | | ptr to typeinfo for D |
+----------------> +-----------------------+
| | C::x() |
| +-----------------------+
| | 0 (vbase_offset) |
| +-----------------------+
| | -20 (top_offset) |
| +-----------------------+
| | ptr to typeinfo for D |
+-------------> +-----------------------+
| A::v() |
+-----------------------+
Конструктор D передает указатель на VTT D на конструктор B (в этом случае он передает адрес первого B-in-D запись).И действительно, vtable, который использовался для макета объекта выше, является специальным vtable, используемым только для построения B-in-D.
Элемент управления возвращается в конструктор D и вызывает конструктор C(с параметром адреса VTT, указывающим на запись "C-in-D + 12").Когда конструктор C завершает работу с объектом, он выглядит следующим образом:
B-in-D
+-----------------------+
| 20 (vbase_offset) |
+-----------------------+
| 0 (top_offset) |
+-----------------------+
| ptr to typeinfo for B |
+---------------------------------> +-----------------------+
| | B::w() |
| +-----------------------+
| C-in-D | 0 (vbase_offset) |
| +-----------------------+ +-----------------------+
d --> +----------+ | | 12 (vbase_offset) | | -20 (top_offset) |
| vtable |--+ +-----------------------+ +-----------------------+
+----------+ | 0 (top_offset) | | ptr to typeinfo for B |
| b | +-----------------------+ +-----------------------+
+----------+ | ptr to typeinfo for C | | A::v() |
| vtable |--------> +-----------------------+ +-----------------------+
+----------+ | C::x() |
| c | +-----------------------+
+----------+ | 0 (vbase_offset) |
| | +-----------------------+
+----------+ | -12 (top_offset) |
| vtable |--+ +-----------------------+
+----------+ | | ptr to typeinfo for C |
| a | +-----> +-----------------------+
+----------+ | A::v() |
+-----------------------+
Как видите, конструктор C снова изменил указатель vtable встроенного A-объекта. Встроенные объекты C и A теперь используют специальную конструкцию C-in-D vtable, а внедренный объект B использует специальную конструкцию B-in-D vtable.Наконец, конструктор D завершает работу, и мы получаем ту же диаграмму, что и раньше:
+-----------------------+
| 20 (vbase_offset) |
+-----------------------+
| 0 (top_offset) |
+-----------------------+
| ptr to typeinfo for D |
+----------> +-----------------------+
d --> +----------+ | | B::w() |
| vtable |----+ +-----------------------+
+----------+ | D::y() |
| b | +-----------------------+
+----------+ | 12 (vbase_offset) |
| vtable |---------+ +-----------------------+
+----------+ | | -8 (top_offset) |
| c | | +-----------------------+
+----------+ | | ptr to typeinfo for D |
| d | +-----> +-----------------------+
+----------+ | C::x() |
| vtable |----+ +-----------------------+
+----------+ | | 0 (vbase_offset) |
| a | | +-----------------------+
+----------+ | | -20 (top_offset) |
| +-----------------------+
| | ptr to typeinfo for D |
+----------> +-----------------------+
| A::v() |
+-----------------------+
Разрушение происходит таким же образом, но в обратном порядке.Деструктор D вызывается.После запуска кода уничтожения пользователя деструктор вызывает деструктор C и предписывает ему использовать соответствующую часть VTT D.Деструктор Си манипулирует указателями vtable так же, как и во время конструирования;то есть соответствующие указатели vtable теперь указывают на v-конструкцию C-in-D.Затем он запускает код уничтожения пользователя для C и возвращает управление деструктору D, который затем вызывает деструктор B со ссылкой на VTT D.Деструктор B устанавливает соответствующие части объекта для ссылки на виртуальную таблицу конструкции B-in-D.Он запускает код уничтожения пользователя для B и возвращает управление деструктору D, который, наконец, вызывает деструктор A.Деструктор A изменяет виртуальную таблицу для части A объекта, чтобы она ссылалась на виртуальную таблицу для A. Наконец, управление возвращается к деструктору D, и уничтожение объекта завершено.Память, однажды использованная объектом, возвращается в систему.
Теперь, на самом деле, история несколько сложнее. Вы когда-нибудь видели эти «ответственные» и «не отвечающие» спецификации конструкторов и деструкторов в сообщениях о предупреждениях и ошибках, создаваемых GCC, или в двоичных файлах, создаваемых GCC? Дело в том, что может быть две реализации конструктора и до трех реализаций деструктора.
Конструктор "ответственный" (или полный объект) - это конструктор виртуальных баз, а конструктор "не отвечающий" (или базовый объект) - это конструктор, который этого не делает. Рассмотрим наш пример выше. Если B создается, его конструктор должен вызвать конструктор A, чтобы построить его. Точно так же конструктор C должен построить A. Однако, если B и C построены как часть конструкции D, их конструкторы не должны конструировать A, потому что A является виртуальной базой, а конструктор D позаботится о ее построении ровно один раз. для случая D. Рассмотрим случаи:
Если вы делаете новый A, вызывается «ответственный» конструктор A для создания A.
Когда вы делаете новый B, вызывается «ответственный» конструктор B. Он вызовет конструктор "не отвечает" для A.
новый C похож на новый B.
Новый D вызывает конструктор D "ответственный". Прошелся по этому примеру. «Ответственный» конструктор D вызывает «незавершенные» версии конструкторов A, B и C (в том порядке).
Деструктор "в зарядке" является аналогом конструктора "в плате" - он отвечает за разрушение виртуальных баз. Точно так же деструктор "не отвечает" генерируется. Но есть и третий. Деструктор «ответственного за удаление» - это тот, который освобождает хранилище, а также разрушает объект. Так когда же один из них вызывается по отношению к другому?
Ну, есть два типа объектов, которые могут быть разрушены - те, которые расположены в стеке, и те, которые размещены в куче. Рассмотрим этот код (учитывая нашу алмазную иерархию с виртуальным наследованием ранее):
D d; // allocates a D on the stack and constructs it
D *pd = new D; // allocates a D in the heap and constructs it
/* ... */
delete pd; // calls "in-charge deleting" destructor for D
return; // calls "in-charge" destructor for stack-allocated D
Мы видим, что фактический оператор удаления вызывается не кодом, выполняющим удаление, а скорее деструктором удаления для удаляемого объекта. Почему это так? Почему бы не вызвать вызывающего деструктора, а затем удалить объект? Тогда у вас будет только две копии реализации деструктора вместо трех ...
Хорошо, компилятор мог бы сделать такую вещь, но это было бы более сложным по другим причинам. Рассмотрим этот код (в предположении виртуального деструктора, который вы всегда используете, верно? ... правильно?!?):
D *pd = new D; // allocates a D in the heap and constructs it
C *pc = d; // we have a pointer-to-C that points to our heap-allocated D
/* ... */
delete pc; // call destructor thunk through vtable, but what about delete?
Если у вас не было варианта «ответственного удаления» деструктора D, то операция удаления должна была бы настроить указатель так же, как это делает деструктор thunk. Помните, объект C встроен в D, и поэтому наш указатель на C выше настроен так, чтобы указывать на середину объекта D. Мы не можем просто удалить этот указатель, так как это не указатель, который был возвращено malloc()
когда мы его построили.
Итак, если бы у нас не было ответственного за удаление деструктора, нам потребовалось бы большое спасибо оператору удаления (и представить их в наших таблицах) или что-то подобное.
Thunks, виртуальные и не виртуальные
Этот раздел еще не написан.
Множественное наследование с виртуальными методами на одной стороне
Хорошо. Последнее упражнение Что если у нас есть иерархия наследования алмазов с виртуальным наследованием, как и раньше, но с виртуальными методами только по одной ее стороне? Итак:
class A {
public:
int a;
};
class B : public virtual A {
public:
int b;
virtual void w();
};
class C : public virtual A {
public:
int c;
};
class D : public B, public C {
public:
int d;
virtual void y();
};
В этом случае макет объекта выглядит следующим образом:
+-----------------------+
| 20 (vbase_offset) |
+-----------------------+
| 0 (top_offset) |
+-----------------------+
| ptr to typeinfo for D |
+----------> +-----------------------+
d --> +----------+ | | B::w() |
| vtable |----+ +-----------------------+
+----------+ | D::y() |
| b | +-----------------------+
+----------+ | 12 (vbase_offset) |
| vtable |---------+ +-----------------------+
+----------+ | | -8 (top_offset) |
| c | | +-----------------------+
+----------+ | | ptr to typeinfo for D |
| d | +-----> +-----------------------+
+----------+
| a |
+----------+
Таким образом, вы можете видеть подобъект C, который не имеет виртуальных методов, все еще имеет vtable (хотя и пустой). Действительно, все экземпляры C имеют пустой vtable.