Детали реализации виртуальной диспетчеризации
Прежде всего, я хочу пояснить, что понимаю, что нет понятия vtables и vptrs в стандарте С++. Однако я думаю, что практически все реализации реализуют механизм виртуальной диспетчеризации практически так же (исправьте меня, если я ошибаюсь, но это не главный вопрос). Кроме того, я считаю, что знаю, как работают виртуальные функции, то есть я всегда могу сказать, какую функцию вызывать, мне просто нужны детали реализации.
Предположим, кто-то спросил меня следующее:
"У вас есть базовый класс B с виртуальными функциями v1, v2, v3 и производным классом D: B, который переопределяет функции v1 и v3 и добавляет виртуальную функцию v4. Объясните, как работает виртуальная диспетчеризация".
Я бы ответил так:
Для каждого класса с виртуальными функциями (в данном случае B и D) мы имеем отдельный массив указателей на функции, называемый vtable.
Vtable для B будет содержать
&B::v1
&B::v2
&B::v3
vtable для D будет содержать
&D::v1
&B::v2
&D::v3
&D::v4
Теперь класс B содержит указатель на элемент vptr. D естественно наследует его и, следовательно, содержит его тоже. В constuctor и деструкторе B B устанавливает vptr, чтобы указать на B vtable. В конструкторе и деструкторе D D он указывает на D vtable.
Любой вызов виртуальной функции f объекта x полиморфного класса X интерпретируется как вызов x.vptr [f position in vtables]
Вопросы:
1. Есть ли ошибки в приведенном выше описании?
2. Как компилятор знает позицию f в таблице vtable (подробно, пожалуйста)
3. Означает ли это, что если класс имеет две базы, то он имеет два vptrs? Что происходит в этом случае? (попробуйте описать так же, как я, насколько это возможно)
4. Что происходит в алмазной иерархии с A сверху B, C в середине и D внизу? (A - виртуальный базовый класс B и C)
Спасибо заранее.
Ответы
Ответ 1
1. У меня есть ошибки в приведенном выше описании?
Все хорошо.: -)
2. Как компилятор знает позицию f в таблице vtable
У каждого поставщика будет свой собственный способ сделать это, но я всегда думаю о том, что vtable является отображением символа функции-члена в смещение памяти. Поэтому компилятор просто поддерживает этот список.
3. Означает ли это, что если класс имеет две базы, то он имеет два vptrs? Что происходит в этом случае?
Как правило, компиляторы составляют новую vtable, которая состоит из всех vtables виртуальных баз, добавленных вместе в том порядке, в котором они были указаны, вместе с указателем vtable виртуальной базы. Они следуют этому с помощью функций vtable класса-получателя. Это особенно специфично для поставщика, но для class D : B1, B2
вы обычно видите D._vptr[0] == B1._vptr
.
![multiple inheritance]()
Это изображение фактически предназначено для компоновки полей-членов объекта, но vtables может быть составлено компилятором точно так же (насколько я понимаю).
4. Что происходит в алмазной иерархии с A сверху B, C посередине и D внизу? (A - виртуальный базовый класс B и C)
Короткий ответ? Абсолютный ад. Вы фактически наследовали обе базы? Только один из них? Никто из них? В конечном счете, используются одни и те же методы составления таблицы vtable для класса, но как это делается, он варьируется в зависимости от того, как это должно быть сделано, вовсе не каменное. Существует достойное объяснение решения проблемы иерархии бриллиантов здесь, но, как и большинство из этого, он довольно специфичен для вендора.
Ответ 2
- Выглядит хорошо для меня.
-
Реализация специфична, но большинство из них только в порядке исходного кода - это означает, что порядок, который они отображаются в классе, начинается с базового класса, а затем добавляет новые производные от виртуальных функций. Пока компилятор имеет детерминированный способ сделать это, то все, что он хочет сделать, прекрасно. Однако в Windows для создания COM-совместимых V-таблиц он должен быть в порядке источника
-
(не уверен)
- (гадание) Алмаз просто означает, что у вас может быть две копии базового класса B. Виртуальное наследование объединит их в один экземпляр. Поэтому, если вы установите член через D1, вы можете прочитать его через D2. (с C, полученным из D1, D2, каждый из которых получен из B). Я считаю, что в обоих случаях vtables были бы идентичны, так как указатели на функции одинаковы - память для членов данных - это то, что слилось.
Ответ 3
Комментарии:
-
Я не думаю, что в него входят деструкторы!
-
Вызов, такой как, например, D d; d.v1();
, вероятно, не будет реализован через vtable, поскольку компилятор может разрешить адрес функции в момент компиляции/ссылки.
-
Компилятор знает позицию f
, потому что она там была!
-
Да, класс с несколькими базовыми классами, как правило, имеет несколько vptrs (предполагая виртуальные функции в каждом базовом классе).
-
"Эффективные книги C++" Скотта Мейерса объясняют множественное наследование и бриллианты лучше, чем я могу; Я бы рекомендовал прочитать их по этой (и многим другим) причинам. Подумайте о том, что они важны для чтения!