Зачем виртуальному наследованию нужна vtable, даже если виртуальные функции не задействованы?

Я прочитал этот вопрос: Проблема с размером объекта наследования виртуального класса C ++ и было интересно, почему виртуальное наследование приводит к дополнительному указателю vtable в классе.

Я нашел здесь статью: https://en.wikipedia.org/wiki/Virtual_inheritance

что говорит нам:

Однако это смещение в общем случае может быть известно только во время выполнения ...

Я не понимаю, что здесь связано со средой выполнения. Полная иерархия наследования классов уже известна во время компиляции. Я понимаю виртуальные функции и использование базового указателя, но с виртуальным наследованием такого нет.

Может кто-нибудь объяснить, почему некоторые компиляторы (Clang / GCC) реализуют виртуальное наследование с помощью vtable и как это используется во время выполнения?

Кстати, я также видел этот вопрос: vtable в случае виртуального наследования, но он указывает только на ответы, относящиеся к виртуальным функциям, а это не мой вопрос.


person Klaus    schedule 13.08.2019    source источник
comment
Примечание: vtable / vptr - это детали реализации. Компилятору не требуется их использовать, если они каким-то образом могут реализовать поведение, требуемое стандартом.   -  person Jesper Juhl    schedule 13.08.2019
comment
@ RadosławCybulski: Вы ошибаетесь, поэтому, пожалуйста, пройдите по ссылкам, которые я дал. Вопрос явно показывает, что задействована vtable без использования какой-либо виртуальной функции.   -  person Klaus    schedule 13.08.2019
comment
Спасибо, что указали на несвязанный ответ и отметили как повторяющийся. Речь идет о виртуальном наследовании, а не о виртуальной функции!   -  person Klaus    schedule 13.08.2019
comment
@Klaus Люди делают ошибки или иногда путаются. Не забывайте сохранять вежливость и терпеливо относиться к своим комментариям и правкам.   -  person François Andrieux    schedule 13.08.2019
comment
Что-то в этом вопросе действительно беспокоит людей, в том числе и меня. При чтении 2 это имеет смысл и НЕ соответствует дубликату. Я не могу придумать ничего, что можно было бы предложить изменить. Кроме опечатки в названии. Фиксированный.   -  person user4581301    schedule 13.08.2019
comment
@ user4581301 Вопрос ясный и понятный. Иногда люди просто случайно спрашивали или видели что-то подобное, поэтому при закрытии бросайте пистолет.   -  person François Andrieux    schedule 13.08.2019
comment
@ FrançoisAndrieux: Согласен! Но иногда бывает довольно сложно снова задать вопрос. Я прямо заметил, что речь не идет о виртуальных функциях ...   -  person Klaus    schedule 13.08.2019
comment
Если это помогает (или добавляет путаницы), проблема с ромбами увеличивает размер указателя вдвое по сравнению с размером класса как для gcc, так и для clang: wandbox.org/permlink/XIQzLBjhYJijc5G3   -  person Yksisarvinen    schedule 13.08.2019
comment
Похоже, в этом ответе может быть то, что вам не хватает: stackoverflow.com/a/10905259/4342498   -  person NathanOliver    schedule 13.08.2019
comment
Виртуальные базы и виртуальные функции очень похожи: это отношения, которые можно переопределить. Причина для записей vtable виртуальных функций и записей vtable виртуальных баз в точности одна и та же: разрешить динамическое поведение, основанное только на знании статического типа.   -  person curiousguy    schedule 17.08.2019


Ответы (3)


Полная иерархия наследования классов уже известна во время компиляции.

Достаточно верно; поэтому, если компилятор знает тип наиболее производного объекта, он знает смещение каждого подобъекта в этом объекте. Для этой цели vtable не требуется.

Например, если B и C фактически являются производными от A, а D является производным от обоих B и C, то в следующем коде:

D d;
A* a = &d;

преобразование из D* в A* - это самое большее добавление статического смещения к адресу.

Однако теперь рассмотрим эту ситуацию:

A* f(B* b) { return b; }
A* g(C* c) { return c; }

Здесь f должен иметь возможность принимать указатель на любой B объект, включая объект B, который может быть подобъектом объекта D или некоторого другого объекта наиболее производного класса. При компиляции f компилятор не знает полного набора производных классов B.

Если объект B является наиболее производным объектом, то подобъект A будет расположен с определенным смещением. Но что, если объект B является частью объекта D? Объект D содержит только один объект A, и он не может быть расположен на своем обычном смещении от обоих подобъектов B и C. Таким образом, компилятор должен выбрать местоположение для A подобъекта D, а затем он должен предоставить механизм, чтобы некоторый код с B* или C* мог узнать, где находится подобъект A. Это зависит исключительно от иерархии наследования наиболее производного типа, поэтому vptr / vtable является подходящим механизмом.

person Brian Bi    schedule 13.08.2019
comment
Хорошая точка зрения! Альтернативное решение может заключаться в том, чтобы где-то были реализованы функции преобразования, например экземпляры шаблонов для каждого наблюдаемого преобразования. Может быть, посложнее и кода больше, но размер объекта меньше. Хорошо, наличие единственной функции преобразования, которая принимает смещение из vtable, является одним и часто используемым решением. Спасибо! - person Klaus; 13.08.2019
comment
@Klaus Это решение разваливается в разных ситуациях. Учитывайте B* b = (rand() % 2 == 0) ? new B : new D; f(b);. Во время компиляции компилятор не может знать правильное смещение, которое будет использоваться для поиска подобъекта b A в каждой ситуации. - person Miles Budnek; 13.08.2019
comment
@MilesBudnek: Я пойду и подумаю :-) Спасибо! - person Klaus; 13.08.2019
comment
самое большее, добавив статическое смещение к адресу Это немного больше: вам нужно сначала проверить наличие null. - person curiousguy; 19.08.2019

Однако это смещение в общем случае может быть известно только во время выполнения ...

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

Я думаю, что связанная статья в Википедии дает хорошее объяснение с примерами.

Пример кода из этой статьи:

struct Animal {
  virtual ~Animal() = default;
  virtual void Eat() {}
};

// Two classes virtually inheriting Animal:
struct Mammal : virtual Animal {
  virtual void Breathe() {}
};

struct WingedAnimal : virtual Animal {
  virtual void Flap() {}
};

// A bat is still a winged mammal
struct Bat : Mammal, WingedAnimal {
};

Когда вы обрабатываете объект типа Bat, компилятор может выбирать компоновку объекта различными способами.

Опция 1

+--------------+
| Animal       |
+--------------+
| vpointer     |
| Mammal       |
+--------------+
| vpointer     |
| WingedAnimal |
+--------------+
| vpointer     |
| Bat          |
+--------------+

Вариант 2

+--------------+
| vpointer     |
| Mammal       |
+--------------+
| vpointer     |
| WingedAnimal |
+--------------+
| vpointer     |
| Bat          |
+--------------+
| Animal       |
+--------------+

Значения, содержащиеся в vpointer в Mammal и WingedAnimal, определяют смещения к подобъекту Animal. Эти значения не могут быть известны до времени выполнения, потому что конструктор Mammal не может знать, является ли субъект Bat или каким-либо другим объектом. Если подобъект Monkey, он не будет производным от WingedAnimal. Это будет просто

struct Monkey : Mammal {
};

в этом случае макет объекта может быть:

+--------------+
| vpointer     |
| Mammal       |
+--------------+
| vpointer     |
| Monkey       |
+--------------+
| Animal       |
+--------------+

Как можно видеть, смещение от подобъекта Mammal к подобъекту Animal определяется классами, производными от Mammal. Следовательно, его можно определить только во время выполнения.

person R Sahu    schedule 13.08.2019

Полная иерархия наследования классов уже известна во время компиляции. Но все vptr связанные операции, такие как получение смещений виртуального базового класса и вызов виртуальной функции, откладываются до времени выполнения, потому что только во время выполнения мы можем узнать фактический тип объекта.

Например,

class A() { virtual bool a() { return false; } };
class B() : public virtual A { int a() { return 0; } };
B* ptr = new B();

// assuming function a()'s index is 2 at virtual function table
// the call
ptr->a();

// will be transformed by the compiler to (*ptr->vptr[2])(ptr)
// so a right call to a() will be issued according to the type of the object ptr points to
person Silentroar    schedule 17.04.2021
comment
Это очень верно и стоит упомянуть. К сожалению, OP довольно громко исключает ответы о виртуальных функциях. Фактически, для виртуального наследования требуется vtable, даже если нет виртуальной функции, как указано в других ответах. Основная причина та же: макет класса зависит от того, является ли он частью класса-потомка, следовательно, является динамическим. - person Maëlan; 17.04.2021
comment
@ Maëlan Я согласен с тобой. В принятом ответе указано, что если компилятор знает тип наиболее производного объекта, то он знает смещение каждого подобъекта в этом объекте. Для этой цели vtable не требуется, что вводит в заблуждение, потому что компилятор не знает и не заботится о типе объекта, на который указывает a. Он проверяет только статический тип указателя, то есть A, а затем преобразует vptr связанные операции. К сожалению, у меня недостаточно репутации, чтобы комментировать - person Silentroar; 18.04.2021
comment
Вот, возьми. :-) Насколько я понимаю, упомянутый ответ заключается в том, что достойный компилятор C ++ оптимизирует операции, чтобы обойти vtable, когда он статически знает динамический тип объекта, как в данном примере D d; A* a = &d;. Таким образом, он действительно заботится об отслеживании динамического типа, когда это возможно, в целях оптимизации, хотя, конечно, это невозможно в общем случае. - person Maëlan; 18.04.2021
comment
Я проясню себя. Компилятору действительно необходимо знать тип d, чтобы выполнить правильное преобразование A* a = &d. Но я не думаю, что это связано с оптимизацией компилятора. И после завершения преобразования код, сгенерированный компилятором, не заботится о том, на что указывает a, то есть о его динамическом типе, потому что a обрабатывается так, как если бы он указывает на объект типа A - person Silentroar; 19.04.2021
comment
@ Maëlan для виртуального наследования требуется vtable, даже если нет виртуальной функции, как указано в других ответах Как нужна реальная vtable? Использует ли официальный ABI Microsoft? - person curiousguy; 04.05.2021
comment
@curiousguy TBH Я сам не разбираюсь в C ++, я только читаю эти ответы, и они мне понятны. Теперь я совершенно убежден, что в общем случае вам действительно нужна часть информации, которую предоставляет vtable, даже если вы можете оптимизировать ее в особых случаях. Все, что является динамическим, неразрешимо во время компиляции, поэтому оно динамическое. Что касается Microsoft ABI, я не знаю, все, что я могу сказать, это то, что результаты поиска Google подразумевают, что это так (1, 2 и так далее). - person Maëlan; 04.05.2021
comment
Однако это в статье упоминается __declspec(novtable), «подсказка по оптимизации для Microsoft, которая сообщает компилятору: этот класс никогда не используется сам по себе, а только как базовый класс для других классов, так что не беспокойтесь обо всем этом vtable, спасибо ты." Может быть, вы об этом думали? - person Maëlan; 04.05.2021