понимание vptr в множественном наследовании? - PullRequest
16 голосов
/ 16 апреля 2011

Я пытаюсь разобраться в утверждении в книге эффективного c ++. Ниже приведена диаграмма наследования для множественного наследования.

enter image description here

enter image description here

Теперь в книге сказано, что для каждого vptr требуется отдельная память в каждом классе. И делает следующее утверждение

Странность на приведенной выше диаграмме состоит в том, что существует только три vptr, даже если участвуют четыре класса. Реализации могут свободно генерировать четыре vptr, если им нравится, но трех достаточно (получается что B и D могут совместно использовать vptr), и большинство реализаций используют эту возможность для уменьшения накладных расходов, создаваемых компилятором.

Я не вижу причин, по которым для каждого класса требуется отдельная память для vptr. У меня было понимание, что vptr наследуется от базового класса, независимо от типа наследования. Если мы предположим, что он показал результирующую структуру памяти с унаследованным vptr, как они могут сделать утверждение, что

B и D могут совместно использовать vptr

Может кто-нибудь объяснить немного о vptr в множественном наследовании?

  • Нужен ли отдельный vptr в каждом классе?
  • Также, если выше верно, почему B и D могут делиться vptr?

Ответы [ 5 ]

22 голосов
/ 16 апреля 2011

Ваш вопрос интересен, однако я боюсь, что вы ставите слишком большие цели в качестве первого вопроса, поэтому я отвечу в несколько шагов, если вы не возражаете:)

Отказ от ответственности: я не пишу компилятор, и хотя я, конечно, изучал этот вопрос, мое слово следует воспринимать с осторожностью. Будут мне неточности. И я не очень хорошо разбираюсь в 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 имели один и тот же подобъект. Как это можно сделать?

  1. Помните, что размер класса должен быть постоянным
  2. Помните, что при проектировании ни B, ни C не могут предвидеть, будут ли они использоваться вместе или нет

Решение, которое было найдено, поэтому простое: B и C оставляют только место для указателя на V и:

  • если вы создадите автономный B, конструктор выделит V в куче, которая будет обрабатываться автоматически
  • если вы строите B как часть D, то подпункт B будет ожидать, что конструктор D передаст указатель на местоположение V

И то же самое для 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's 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's

Следовательно, vptr здесь указывало на vtable таблицы D, и, таким образом, foo применялся к D's. Для B оно было полностью прозрачным.

Здесь B и D совместно используют один и тот же vptr!

4. Виртуальные таблицы в мульти-наследовании

К сожалению, это не всегда возможно.

Во-первых, как мы видели, в случае виртуального наследования «общий» элемент странным образом позиционируется в конечном законченном объекте. Поэтому у него есть свой vptr. Это 1 .

Во-вторых, в случае множественного наследования первая база выравнивается по всему объекту, но вторая база не может быть (им обоим нужно место для своих данных), поэтому она не может совместно использовать свой vptr. Это 2 .

В-третьих, первая база выровнена по всему объекту, что дает нам ту же компоновку, что и в случае простого наследования (та же возможность оптимизации). Это 3 .

Довольно просто, нет?

1 голос
/ 16 апреля 2011

Если у класса есть виртуальные члены, нужно найти способ найти их адрес.Они собраны в постоянной таблице (vtbl), адрес которой хранится в скрытом поле для каждого объекта (vptr).Вызов к виртуальному члену, по сути:

obj->_vptr[member_idx](obj, params...);

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

obj->_vptr[member_idx](obj, params...);

, а вызов нового виртуального участника:

obj->_vptr2[member_idx](obj, params...);

Если база не виртуальная, можно организовать второй vtblставится сразу после первого, эффективно увеличивая размер втбл.И _vptr2 больше не нужен.Таким образом, вызов нового виртуального члена:

obj->_vptr[member_idx+num_inherited_members](obj, params...);

В случае (не виртуального) множественного наследования один наследует два vtbl и два vptr.Они не могут быть объединены, и вызовы должны обратить внимание, чтобы добавить смещение к объекту (чтобы унаследованные элементы данных были найдены в правильном месте).Звонки первым членам базового класса будут

obj->_vptr_base1[member_idx](obj, params...);

, а вторым

obj->_vptr_base2[member_idx](obj+offset, params...);

Новые виртуальные члены снова могут быть либо помещены в новый vtbl, либо добавлены в vtbl ofпервая база (чтобы в будущих вызовах не было смещений).

Если база виртуальная, нельзя добавить новый vtbl к унаследованной, так как это может привести к конфликтам (в приведенном вами примере, если и B, и C добавляют свои виртуальные функции, как D сможет построить свою версию?).

Таким образом, A требуется vtbl.B и C нужен vtbl, и он не может быть добавлен к одному из A, потому что A является виртуальной базой обоих.D нужен vtbl, но его можно добавить к B, так как B не является виртуальным базовым классом D.

0 голосов
/ 28 июня 2011

Я думаю, что D нужно 2 или 3 vptrs.

Здесь A может требовать или не требовать vptr. B нужен тот, который не должен использоваться совместно с A (потому что A фактически наследуется). C нужен тот, который не должен использоваться совместно с A (то же самое). D может использовать B или C vftable для своих новых виртуальных функций (если таковые имеются), поэтому он может делиться B или C.

Моя старая статья «C ++: Под капюшоном» объясняет реализацию Microsoft C ++ виртуальных базовых классов. http://www.openrce.org/articles/files/jangrayhood.pdf

И (MS C ++) вы можете скомпилировать с помощью cl / d1reportAllClassLayout, чтобы получить текстовый отчет о классах памяти классов.

Счастливого взлома!

0 голосов
/ 16 апреля 2011

Я не мог видеть никаких причин, почему там это требование отдельной памяти в каждый класс для вптр

Во время выполнения, когда вы вызываете (виртуальный) метод через указатель, ЦПУ не знает о реальном объекте, на который направлен метод. Если у вас есть B* b = ...; b->some_method();, тогда переменная b может потенциально указывать на объект, созданный с помощью new B() или new D() или even new E(), где E - некоторый другой класс, который наследует от (либо) B, либо D. Каждый из этих классов может предоставить свою собственную реализацию (переопределение) для some_method(). Таким образом, вызов b->some_method() должен отправлять реализацию из B, D или E в зависимости от объекта, на который указывает b.

vptr объекта позволяет процессору найти адрес реализации some_method, который действует для этого объекта. Каждый класс определяет свой собственный vtbl (содержащий адреса всех виртуальных методов), и каждый объект класса начинается с vptr, который указывает на этот vtbl.

0 голосов
/ 16 апреля 2011

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

Класс B и класс C будут иметь совершенно разные структуры виртуальных таблиц, поскольку у них разные методы. Виртуальная таблица для класса D может выглядеть как виртуальная таблица для класса B, за которой следуют дополнительные данные для методов класса C.

Когда вы генерируете объект класса D, вы можете привести его как указатель на B или как указатель на C, или даже как указатель на класс A. Вы можете передать эти указатели модулям, которые даже не знают о существовании класса D, но может вызывать методы класса B или C или A. Эти модули должны знать, как найти указатель на виртуальную таблицу класса, и они должны знать, как найти указатели на методы класса B / C / А в виртуальной таблице. Вот почему вам нужно иметь отдельные VPTR для каждого класса.

Класс D хорошо осведомлен о существовании класса B и структуры его виртуальной таблицы и поэтому может расширять его структуру и повторно использовать VPTR из объекта B.

Когда вы приводите указатель на объект D к указателю на объект B или C или A, он фактически обновляет указатель на некоторое смещение, так что он начинается с vptr, соответствующего этому конкретному базовому классу.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...