当我从虚拟基派生 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
子对象。要确定如何调整 d1
和 d2
以找到指向 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
几乎继承自 B
,F
继承自 E
和 D
,这样 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.
我正在使用 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
子对象。要确定如何调整 d1
和 d2
以找到指向 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
几乎继承自 B
,F
继承自 E
和 D
,这样 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.