当我从虚拟基派生 D 时,为什么 VS2015 中的 sizeof(D) 增加了 8 个字节?

Why the sizeof(D) increased by 8 bytes in VS2015 when I derived D from a virtual base?

我正在使用 C++14 §3.11/2 中的示例:

struct B { long double d; };
struct D : virtual B { char c; }

在 运行 下面的代码片段之后,在 clang、g++ 和 VS2015 中

#include <iostream>
struct B { long double d; };
struct D : /*virtual*/ B { char c; };

int main()
{
    std::cout << "sizeof(long double) = " << sizeof(long double) << '\n';
    std::cout << "alignof(long double) = " << alignof(long double) << '\n';

    std::cout << "sizeof(B) = " << sizeof(B) << '\n';
    std::cout << "alignof(B) = " << alignof(B) << '\n';

    std::cout << "sizeof(D) = " << sizeof(D) << '\n';
    std::cout << "alignof(D) = " << alignof(D) << '\n';
}

我得到了以下结果:

                         clang           g++         VS2015  
sizeof(long double)        16             16            8
alignof(long double)       16             16            8
sizeof(B)                  16             16            8
alignof(B)                 16             16            8
sizeof(D)                  32             32           16
alignof(D)                 16             16            8

现在,在取消注释上面代码中struct D定义中的virtual和运行clang、g++和VS2015的代码后,我得到了以下结果:

                         clang           g++         VS2015  
sizeof(long double)        16             16            8
alignof(long double)       16             16            8
sizeof(B)                  16             16            8
alignof(B)                 16             16            8
sizeof(D)                  32             32           24
alignof(D)                 16             16            8

我对上面得到的结果毫无疑问,只有一个例外:为什么sizeof(D)在VS2015中从16增加到24?

我知道这是实现定义的,但对于这种大小的增加可能有一个合理的解释。如果可能的话,这是我想知道的。

当存在 virtual 基础对象时,基础对象相对于派生对象地址的位置是不可静态预测的。值得注意的是,如果您稍微扩展 class 层次结构,很明显可以有多个 D 子对象,它们仍然只需要引用一个 B 基础对象:

class I1: public D {};
class I2: public D {};
class Most: public I1, public I2 {};

您可以通过首先转换为 I1 或首先转换为 I2:

Most 对象获得 D*
Most m;
D*   d1 = static_cast<I1*>(&m);
D*   d2 = static_cast<I2*>(&m);

您将有 d1 != d2,即确实有两个 D 子对象,但是 static_cast<B*>(d1) == static_cast<B*>(d2),即只有一个 B 子对象。要确定如何调整 d1d2 以找到指向 B 子对象的指针,需要动态偏移。有关如何确定此偏移量的信息需要存储在某处。此信息的存储可能是额外 8 个字节的来源。

我认为 MSVC++ 中类型的对象布局没有[公开]记录,也就是说,不可能确定它们在做什么。从外观上看,他们嵌入了一个 64 位对象,以便能够分辨出基对象相对于派生对象地址的位置(指向某些类型信息的指针、指向基的指针、基的偏移量等)像那样)。其他 8 个字节很可能源于需要存储 char 加上一些填充以使对象在合适的边界对齐。这看起来与其他两个编译器所做的相似,除了它们使用 16 个字节作为 long double 的开头(可能只是 10 个字节填充到合适的对齐方式)。

要了解 C++ 对象模型的工作原理,您可能想看看 Stan Lippman 的 "Inside the C++ Object Model"。它有点过时但描述了潜在的实现技术。我不知道 MSVC++ 是否使用其中任何一个,但它给出了可能使用的想法。

对于gcc and clang you can have a look at the Itanium ABI使用的对象模型:他们基本上使用Itanium ABI,对实际使用的CPU.

稍作调整

如果你真的利用虚拟继承的虚拟方面,我认为对 vtable 指针的需求变得很清楚。 vtable 中的一项可能是 B 开始与 D.

开始的偏移量

假设 E 几乎继承自 BF 继承自 ED,这样 D 内的 F 最终使用 E 中的 B 作为基数 class。在不知道它是 F 的基础 class 的 D 方法中,如果 vtable 中没有存储信息,您如何找到 B 的成员?

所以 clang 和 G++ 将 8 个字节的 padding 更改为 vtable 指针,你认为没有变化。但是 VS2015 从来没有那个填充,所以它需要为 vtable 指针添加 8 个字节。

也许编译器注意到 vtable 指针的唯一用途是在计算基指针的低效方案中。所以也许这被优化为简单地拥有一个基指针而不是一个虚表指针。但这不会改变对 8 个字节的需求。

在 visual studio 中,默认行为是所有结构都沿 8 字节边界对齐。 i.e.even 如果你这样做

struct A {
  char c;
}

然后检查sizeof(A),你会看到它是8个字节。

现在,在您的情况下,当您将 struct D 的继承类型更改为虚拟时,编译器必须执行一些额外的操作才能完成此操作。首先,它为结构体 D 创建了一个虚拟 table。这个 vtable 包含什么?它包含一个指向结构 B 在 memory.Next 中的偏移量的指针,它在结构 D 的头部添加了一个指向新创建的 vtable.

的 vptr

因此,现在结构 D 应该如下所示:

 struct D : virtual B { void* vptr; char c; }

因此,D 的大小将为:

sizeof (long double) + sizeof (void*) +  sizeof (char) = 8 + 8 + 1 = 17

这就是我们一开始讨论的边界对齐的用武之地。由于所有结构都必须与 8 字节边界对齐,而结构 D 只有 17 字节,因此编译器会向结构添加 7 个填充字节以使其它与 8 字节边界对齐。

所以大小现在变成:

Size of D = Size of elements of D  + Padding bytes for byte alignment = 17 + 7 = 24 bytes.