Виртуальные таблицы и виртуальные указатели для множественного виртуального наследования и приведения типов
Меня немного смущает vptr и представление объектов в памяти, и я надеюсь, что вы поможете мне лучше разобраться в этом вопросе.
Рассматривать
B
наследуется отA
и оба определяют виртуальные функцииf()
, Из того, что я узнал, представление объекта класса B в памяти выглядит так:[ vptr | A | B ]
иvtbl
тотvptr
указывает на содержитB::f()
, Я также понял, что приведение объекта изB
вA
ничего не делает, кроме игнорированияB
часть в конце объекта. Это правда? Разве это поведение не неправильно? Мы хотим, чтобы этот объект типаA
выполнитьA::f()
метод, а неB::f()
,Есть ли ряд
vtables
в системе как количество классов?Как будет
vtable
класса, который наследует от двух или более классов похожи? Как будет представлен объект C в памяти?То же, что вопрос 3, но с виртуальным наследованием.
3 ответа
Следующее верно для GCC (и, похоже, верно для ссылки LLVM), но может также быть верно для компилятора, который вы используете. Все это зависит от реализации и не регулируется стандартом C++. Тем не менее, GCC пишет свой собственный двоичный стандартный документ, Itanium ABI.
Я попытался объяснить основные понятия о том, как виртуальные таблицы представлены в более простых словах, как часть моей статьи о производительности виртуальных функций в C++, которая может оказаться полезной. Вот ответы на ваши вопросы:
Более правильный способ изобразить внутреннее представление объекта:
| vptr | ======= | ======= | <-- your object |----A----| | |---------B---------|
B
содержит свой базовый классA
Он просто добавляет пару своих членов после его окончания.Кастинг из
B*
вA*
на самом деле ничего не делает, он возвращает тот же указатель, иvptr
остается такой же. Но, в двух словах, виртуальные функции не всегда вызываются через vtable. Иногда они вызываются так же, как и другие функции.Вот более подробное объяснение. Вы должны различать два способа вызова функции-члена:
A a, *aptr; a.func(); // the call to A::func() is precompiled! aptr->A::func(); // ditto aptr->func(); // calls virtual function through vtable. // It may be a call to A::func() or B::func().
Дело в том, что во время компиляции известно, как будет вызываться функция: через vtable или просто будет обычным вызовом. И дело в том, что тип выражения приведения известен во время компиляции, и поэтому компилятор выбирает правильную функцию во время компиляции.
B b, *bptr; static_cast<A>(b)::func(); //calls A::func, because the type // of static_cast<A>(b) is A!
В этом случае он даже не заглядывает внутрь vtable!
Как правило, нет. Класс может иметь несколько vtables, если он наследует от нескольких баз, каждая из которых имеет свою собственную vtable. Такой набор виртуальных таблиц образует "группу виртуальных таблиц" (см. Пункт 3).
Классу также нужен набор конструкторских таблиц, чтобы правильно распределять виртуальные функции при построении баз сложного объекта. Вы можете прочитать далее в стандарте, который я связал.
Вот пример. Предполагать
C
наследует отA
а такжеB
каждый класс определяющийvirtual void func()
, так же какa
,b
или жеc
виртуальная функция, соответствующая ее названию.C
будет иметь vtable группу из двух vtables. Он поделится одним vtable сA
(виртуальная таблица, в которую входят собственные функции текущего класса, называется "первичной"), а виртуальная таблица дляB
будет добавлено:| C::func() | a() | c() || C::func() | b() | |---- vtable for A ----| |---- vtable for B ----| |--- "primary virtual table" --||- "secondary vtable" -| |-------------- virtual table group for C -------------|
Представление объекта в памяти будет выглядеть почти так же, как выглядит его vtable. Просто добавьте
vptr
перед каждой vtable в группе, и вы получите приблизительную оценку того, как данные расположены внутри объекта. Вы можете прочитать об этом в соответствующем разделе двоичного стандарта GCC.Виртуальные базы (некоторые из них) выкладываются в конце группы vtable. Это сделано потому, что у каждого класса должна быть только одна виртуальная база, и если они смешаны с "обычными" виртуальными таблицами, то компилятор не сможет повторно использовать части созданных виртуальных таблиц для создания частей производных классов. Это приведет к вычислению ненужных смещений и уменьшит производительность.
Благодаря такому размещению виртуальные базы также вводят в свои таблицы дополнительные элементы:
vcall
смещение (чтобы получить адрес окончательного переопределения при переходе от указателя к виртуальной базе внутри законченного объекта до начала класса, который переопределяет виртуальную функцию) для каждой виртуальной функции, определенной там. Также каждая виртуальная база добавляетvbase
смещения, которые вставляются в vtable производного класса; они позволяют определить, где начинаются данные виртуальной базы (их нельзя предварительно скомпилировать, поскольку фактический адрес зависит от иерархии: виртуальные базы находятся в конце объекта, а смещение от начала зависит от количества не виртуальных классы, которые наследует текущий класс.).
Гав, я надеюсь, я не внес много ненужных сложностей. В любом случае вы можете ссылаться на исходный стандарт или на любой документ вашего собственного компилятора.
- Это кажется правильным для меня. Это не так, если вы используете указатель A, вам нужно только то, что обеспечивает A, плюс, может быть, реализации функций B, которые доступны из v-таблицы A (может быть несколько vtable, в зависимости от компилятора и сложности иерархии).
- Я бы сказал, да, но это зависит от реализации компилятора, поэтому вам не нужно об этом знать.
- и 4. Читайте дальше.
Я бы порекомендовал прочитать множественное наследование, которое считается полезным. Это длинная статья, но она проясняет тему, подробно объясняет, как работает наследование в C++ (ссылки на рисунки не работают, но они доступны внизу страницы).).
Если объект B наследуется от A, то представление памяти для B будет следующим:
- указатель на виртуальную таблицу A
- Конкретные переменные / функции
- указатель на виртуальную таблицу B
- B конкретные переменные / функции / переопределения
Если у вас есть B* b = новый B(); (A)b->f() тогда:
- если f объявлена как виртуальная функция, то вызывается реализация B, потому что b имеет тип B
- если f не был объявлен как виртуальная функция, то при вызове в vtable не будет поиска правильной реализации, и будет вызвана реализация A.
Каждый объект будет иметь свой собственный vtable (не принимайте это как должное, так как я должен исследовать это
Взгляните на это для примера vtable Layour при работе с множественным наследованием
Смотрите это для обсуждения наследования алмазов и представления vtable