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

时间:2011-04-05 14:50: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 ();

7 个答案:

答案 0 :(得分:18)

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

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

void g()
{
    D bar;
    f(&bar);
}

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

如果编译器知道您调用该函数的对象的类型(如您的示例中所示),则应该没有开销,因为可以静态调度函数调用(在编译时)。在您的示例中,bar的动态类型已知为D(它不能是其他任何内容),因此虚拟基类子对象A的偏移量可以在以下位置计算:编译时间。

答案 1 :(得分:12)

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

class A { ... };
class B : public A { ... }

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

| B's stuff | A's stuff |

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

class A { ... };
class B : public virtual A { ... };
class C : public virtual A { ... };
class D : public C, public B { ... };

B的内存布局:

| B's stuff | A's stuff |

C的内存布局:

| C's stuff | A's stuff |

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

| D's stuff | B's stuff | C's stuff | A's stuff |

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

答案 2 :(得分:7)

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

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

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

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

答案 3 :(得分:2)

我认为,虚拟继承没有运行时代价。 不要将虚拟继承与虚函数混淆。两者都是两回事。

虚拟继承可确保您在A的实例中只有一个子对象D。所以我认为单独会对运行时间造成惩罚。

但是,可能会出现在编译时无法识别此子对象的情况,因此在这种情况下,虚拟继承会有运行时间损失。一个这样的案例由 James 在他的回答中描述。

答案 4 :(得分:2)

具体地说,在Microsoft Visual C ++中,指针到成员的大小存在实际差异。 见#pragma pointers_to_members。正如您在该列表中所看到的 - 最常用的方法是“虚拟继承”,它与多重继承不同,后者又与单一继承不同。

这意味着在存在虚拟继承的情况下需要更多信息来解析指向成员的指针,并且如果仅通过CPU缓存中占用的数据量,它将对性能产生影响 - 尽管可能也可以在成员查找的长度或所需的跳跃次数。

答案 5 :(得分:1)

您的问题主要集中在调用虚拟基础的常规函数,而不是虚拟基类虚拟函数的(远)更有趣的情况(A类)在你的例子中) - 但是,可能会有成本。当然,一切都依赖于编译器。

当编译器编译A :: foo时,它假定“this”指向A的数据成员驻留在内存中的起点。此时,编译器可能不知道A类将是任何其他类的虚拟基础。但它很乐意生成代码。

现在,当编译器编译B时,实际上不会有变化,因为当A是虚基类时,它仍然是单继承,在典型情况下,编译器将通过放置A类数据来布局B类成员紧接着是B级的数据成员 - 因此B *可以立即转换为A *而不会有任何价值变化,因此,不需要进行任何调整。编译器可以使用相同的“this”指针调用A :: foo(即使它是B *类型)并且没有任何伤害。

同样的情况是C类 - 它仍然是单一继承,典型的编译器将A的数据成员紧跟C的数据成员,因此C *可以立即转换为A *而不会有任何值的变化。因此,编译器可以简单地使用相同的“this”指针调用A :: foo(即使它是C *类型)并且没有任何伤害。

然而,D类的情况完全不同.D类的布局通常是A类的数据成员,其次是B类的数据成员,其次是C类的数据成员,然后是D类的数据成员。

使用典型的布局,D *可以立即转换为A *,因此A :: foo没有任何代价 - 编译器可以调用它为A :: foo生成的相同例程而无需任何更改“这个”,一切都很好。

但是,如果编译器需要调用C :: other_member_func等成员函数,情况也会发生变化,即使C :: other_member_func是非虚拟的。原因是当编译器为C :: other_member_func编写代码时,它假定“this”指针引用的数据布局是A的数据成员,后面跟着C的数据成员。但对于D的实例来说情况并非如此。编译器可能需要重写并创建一个(非虚拟的)D :: other_member_func,只是为了处理类实例的内存布局差异。

请注意,在使用多重继承时,这是一种不同但相似的情况,但在没有虚拟基础的多重继承中,编译器可以通过简单地向“this”指针添加位移或修正来解决所有问题,以解释基类是“嵌入”在派生类的实例中。但是对于虚拟基础,有时需要重写函数。这完全取决于被调用的(甚至是非虚拟的)成员函数访问哪些数据成员。

例如,如果类C定义了非虚拟成员函数C :: some_member_func,则编译器可能需要编写:

  1. C :: some_member_func从C的实际实例(而不是D)调用时,在编译时确定(因为some_member_func不是虚函数)
  2. C :: some_member_func,当从编译时确定的D类的实际实例调用相同的成员函数时。 (从技术上讲,这个例程是D :: some_member_func。即使这个成员函数的定义是隐式的并且与C :: some_member_func的源代码相同,生成的目标代码也可能略有不同。)
  3. 如果C :: some_member_func的代码碰巧使用在A类和C类中定义的成员变量。

答案 6 :(得分:0)

虚拟继承必须要付出代价。

证明是,实际上继承的类所占的比例大于部分的总和。

典型案例:

struct A{double a;};

struct B1 : virtual A{double b1;};
struct B2 : virtual A{double b2;};

struct C : virtual B1, virtual B2{double c;}; // I think these virtuals are not strictly necessary
static_assert( sizeof(A) == sizeof(double) ); // as expected

static_assert( sizeof(B1) > sizeof(A) + sizeof(double) ); // the equality holds for non-virtual inheritance
static_assert( sizeof(B2) > sizeof(A) + sizeof(double) );  // the equality holds for non-virtual inheritance
static_assert( sizeof(C) > sizeof(A) + sizeof(double) + sizeof(double) + sizeof(double) );
static_assert( sizeof(C) > sizeof(A) + sizeof(double) + sizeof(double) + sizeof(double) + sizeof(double));

https://godbolt.org/z/zTcfoY

还存储了什么?我不太了解 我认为它类似于虚拟表,但是用于访问单个成员。