在调用非虚拟基本方法时,C++中是否有任何惩罚/成本的虚拟继承?

Goo*_*ofy 22 c++ runtime overhead virtual-inheritance

当我们从其基类调用常规函数成员时,在C++中使用虚拟继承是否会在编译代码中产生运行时损失?示例代码:

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 ();
Run Code Online (Sandbox Code Playgroud)

Jam*_*lis 18

可能是,如果通过指针或引用调用成员函数,并且编译器无法绝对确定指针或引用指向或引用的对象类型.例如,考虑:

void f(B* p) { p->foo(); }

void g()
{
    D bar;
    f(&bar);
}
Run Code Online (Sandbox Code Playgroud)

假设调用f没有内联,编译器需要生成代码以找到A虚拟基类子对象的位置以便调用foo.通常这种查找涉及检查vptr/vtable.

如果编译器知道您调用该函数的对象的类型(虽然在您的示例中是这种情况),则应该没有开销,因为可以静态调度函数调用(在编译时).在您的示例中,bar已知动态类型D(它不能是其他任何东西),因此A可以在编译时计算虚拟基类子对象的偏移量.

  • @James:我认为你对使用虚函数的虚拟继承感到困惑,不是吗? (2认同)
  • @Nawaz:不。给一个可能是某个派生类的子对象的任意“B”对象的“B*”,你知道“A”基类子对象相对于那个“B*”的位置吗?在编译时,您不知道 `A` 基类子对象在哪里。 (2认同)
  • @Martin:我的理解是,您只需要类似于`A* a_ptr = b_ptr + b_ptr->vtable[index_of_offset_of_virtual_base_A]`(概念上)的东西。对于给定的派生类(如“D”),“A”子对象位于“B”子对象的已知偏移处。但是,如果您有一个指向某个任意对象的“B*”,您不知道_相对于该指针_“A”子对象所在的位置,而无需检查 vtable,因为最派生的类型可能是“B”或“D”或`[别的东西]`。(我在这里可能没有表达清楚;我今天早上才刚喝过咖啡。) (2认同)

Pup*_*ppy 12

是的,虚拟继承具有运行时性能开销.这是因为对于任何指向对象的指针/引用,编译器在编译时无法找到它的子对象.相反,对于单继承,每个子对象都位于原始对象的静态偏移处.考虑:

class A { ... };
class B : public A { ... }
Run Code Online (Sandbox Code Playgroud)

B的内存布局看起来有点像这样:

| B's stuff | A's stuff |
Run Code Online (Sandbox Code Playgroud)

在这种情况下,编译器知道A的位置.但是,现在考虑一下MVI的情况.

class A { ... };
class B : public virtual A { ... };
class C : public virtual A { ... };
class D : public C, public B { ... };
Run Code Online (Sandbox Code Playgroud)

B的内存布局:

| B's stuff | A's stuff |
Run Code Online (Sandbox Code Playgroud)

C的内存布局:

| C's stuff | A's stuff |
Run Code Online (Sandbox Code Playgroud)

可是等等!当D被实例化时,它看起来不像那样.

| D's stuff | B's stuff | C's stuff | A's stuff |
Run Code Online (Sandbox Code Playgroud)

现在,如果你有一个B*,如果它真的指向一个B,那么A就在B-旁边但是如果它指向一个D,那么为了获得A*你真的需要跳过C子-object,因为任何给定的B*可以在运行时动态指向B或D,那么您将需要动态地更改指针.这至少意味着您必须生成代码以通过某种方式查找该值,而不是在编译时获取值,这是单继承所发生的.


Jer*_*fin 7

至少在典型的实现中,虚拟继承对(至少一些)数据成员的访问带来(小的!)惩罚.特别是,您通常最终会有一个额外的间接级别来访问您虚拟派生的对象的数据成员.这是因为(至少在正常情况下)两个或多个单独的派生类不仅具有相同的基类,而且具有相同的基类对象.为此,两个派生类都指向最大派生对象的相同偏移量,并通过该指针访问这些数据成员.

虽然从技术上讲不是由于虚拟继承,但值得注意的是,对于多重继承,通常存在单独的(再次,小的)惩罚.在继承的典型实现中,在对象的某个固定偏移处有一个vtable指针(通常是最开始的).在多重继承的情况下,你显然不能在同一个偏移处有两个vtable指针,所以你最终会得到一些vtable指针,每个指针位于对象的一个​​单独的偏移处.

IOW,具有单继承的vtable指针通常只是static_cast<vtable_ptr_t>(object_address),但是你得到了多重继承static_cast<vtable_ptr_t>(object_address+offset).

从技术上讲,两者完全是分开的 - 但当然,几乎唯一用于虚拟继承的用途是与多重继承相结合,所以它无论如何都是半相关的.