25

当我们从其基类调用常规函数成员时,在 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 ();
4

7 回答 7

21

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

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

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

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

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

于 2011-04-05T14:56:39.320 回答
14

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

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 sub -object,并且由于任何给定B*的都可以在运行时动态指向 B 或 D,因此您将需要动态更改指针。这至少意味着您必须生成代码以通过某种方式找到该值,而不是在编译时将值烘焙,这就是单继承发生的情况。

于 2011-04-05T15:42:33.500 回答
8

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

尽管从技术上讲这不是由于虚拟继承,但可能值得注意的是,一般来说,多重继承有一个单独的(同样,很小的)惩罚。在继承的典型实现中,您在对象的某个固定偏移处(通常是最开始)有一个 vtable 指针。在多重继承的情况下,你显然不能有两个 vtable 指针在相同的偏移量,所以你最终会得到许多 vtable 指针,每个指针在对象中的一个单独的偏移量处。

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

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

于 2011-04-05T15:49:02.820 回答
2

具体而言,在 Microsoft Visual C++ 中,指向成员的指针大小存在实际差异。请参阅#pragma pointers_to_members。正如您在该清单中看到的 - 最通用的方法是“虚拟继承”,它不同于多重继承,而多重继承又不同于单一继承。

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

于 2011-04-05T15:00:16.583 回答
1

我认为,虚拟继承没有运行时惩罚。不要将虚拟继承与虚拟函数混淆。两者是两种不同的东西。

虚拟继承确保您AD. 所以我不认为单独它会有运行时惩罚。

但是,可能会出现在编译时无法知道该子对象的情况,因此在这种情况下,虚拟继承会在运行时受到惩罚。詹姆斯在他的回答中描述了一个这样的案例。

于 2011-04-05T14:57:04.937 回答
1

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

当编译器编译 A::foo 时,它假定“this”指向 A 的数据成员在内存中的起始位置。此时,编译器可能不知道类 A 将是任何其他类的虚拟基。但它很高兴地生成了代码。

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

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

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

使用典型的布局,一个 D * 可以立即转换为一个 A *,因此对 A::foo 没有任何惩罚——编译器可以调用它为 A::foo 生成的相同例程,而不需要对“this”进行任何更改一切都很好。

但是,如果编译器需要调用诸如 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 的源代码相同,但生成的目标代码可能会略有不同。)

如果 C::some_member_func 的代码碰巧使用了在 A 类和 C 类中定义的成员变量。

于 2018-02-16T09:05:28.823 回答
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

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

于 2020-11-18T04:48:55.463 回答