Есть ли какой-либо штраф/стоимость виртуального наследования в С++ при вызове метода не виртуального базиса?
Имеет ли использование виртуального наследования в С++ ограничение времени выполнения в скомпилированном коде, когда мы вызываем регулярного члена функции из его базового класса? Пример кода:
class A {
public:
void foo(void) {}
};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
// ...
D bar;
bar.foo ();
Ответы
Ответ 1
Может быть, да, если вы вызываете функцию-член через указатель или ссылку, и компилятор не может с абсолютной уверенностью определить, к какому типу объекта относятся указатель или контрольные точки или ссылки. Например, рассмотрим:
void f(B* p) { p->foo(); }
void g()
{
D bar;
f(&bar);
}
Предполагая, что вызов f
не указан, компилятору необходимо сгенерировать код, чтобы найти местоположение субобъекта виртуального базового класса A
, чтобы вызвать foo
. Обычно этот поиск включает проверку vptr/vtable.
Если компилятор знает тип объекта, в котором вы вызываете функцию, хотя (как и в вашем примере), не должно быть накладных расходов, потому что вызов функции может быть отправлен статически (во время компиляции). В вашем примере динамический тип bar
известен как D
(он не может быть кем-то еще), поэтому смещение субобъекта виртуального базового класса A
можно вычислить во время компиляции.
Ответ 2
Да, виртуальное наследование имеет служебные данные о производительности во время выполнения. Это связано с тем, что компилятор для любого указателя/ссылки на объект не может найти его под-объекты во время компиляции. В constrast, для одиночного наследования, каждый под-объект находится со статическим смещением исходного объекта. Рассмотрим:
class A { ... };
class B : public A { ... }
Макет памяти B выглядит примерно так:
| B stuff | A stuff |
В этом случае компилятор знает, где находится A. Однако теперь рассмотрим случай MVI.
class A { ... };
class B : public virtual A { ... };
class C : public virtual A { ... };
class D : public C, public B { ... };
B макет памяти:
| B stuff | A stuff |
C:
| C stuff | A stuff |
Но подождите! Когда D создается, это не выглядит так.
| D stuff | B stuff | C stuff | A stuff |
Теперь, если у вас есть B *, если он действительно указывает на B, то A находится рядом с B-, но если он указывает на D, то для получения A * вам действительно нужно пропустить суб-объект C, и поскольку любой заданный B*
может динамически указывать на B или D во время выполнения, вам нужно будет динамически изменять указатель. Это, как минимум, означает, что вам придется создавать код, чтобы каким-то образом найти это значение, в отличие от того, чтобы значение было испечено во время компиляции, что и происходит для одиночного наследования.
Ответ 3
По крайней мере, в типичной реализации виртуальное наследование несет (небольшое!) штраф за (по крайней мере некоторый) доступ к членам данных. В частности, вы обычно получаете дополнительный уровень косвенности для доступа к элементам данных объекта, из которого вы получили практически. Это происходит потому, что (по крайней мере, в нормальном случае) два или более отдельных производных класса имеют не только один и тот же базовый класс, но тот же объект базового класса. Для этого оба производных класса имеют указатели на одно и то же смещение в наиболее производный объект и получают доступ к этим элементам данных через этот указатель.
Хотя это технически не из-за виртуального наследования, вероятно, стоит отметить, что существует отдельное (опять же небольшое) наказание за множественное наследование вообще. В типичной реализации одиночного наследования у вас есть указатель vtable при некотором фиксированном смещении в объекте (нередко с самого начала). В случае множественного наследования у вас, очевидно, не может быть двух указателей vtable при одном и том же смещении, поэтому вы получите несколько указателей vtable, каждый с отдельным смещением в объекте.
IOW, указатель vtable с одиночным наследованием обычно равен static_cast<vtable_ptr_t>(object_address)
, но с множественным наследованием вы получаете static_cast<vtable_ptr_t>(object_address+offset)
.
Технически эти два полностью разделены - но, конечно, почти единственное использование виртуального наследования связано с множественным наследованием, поэтому оно все равно равнозначно.
Ответ 4
Я думаю, что для виртуального наследования нет времени исполнения. Не путать виртуальное наследование с виртуальными функциями. Обе эти две вещи.
виртуальное наследование гарантирует, что у вас есть только один под-объект A
в случаях D
. Поэтому я не думаю, что для него будет .
Однако могут возникнуть случаи, когда этот под-объект не может быть известен во время компиляции, поэтому в таких случаях на виртуальное наследование будет наложен штраф времени исполнения. Один из таких случаев описывается Джеймсом в его ответе.
Ответ 5
Конкретно в Microsoft Visual С++ существует реальная разница в размерах указателя на элемент.
См. # pragma pointers_to_members. Как вы можете видеть в этом списке - самым общим методом является "виртуальное наследование", которое отличается от множественного наследования, которое, в свою очередь, отличается от одиночного наследования.
Это означает, что требуется больше информации для разрешения указателя на член в случае наличия виртуального наследования, и оно будет иметь влияние на производительность, если только через объем данных, занятых в кэше ЦП, но, скорее всего, также в длине поиска члена или количества необходимых прыжков.