Bar*_*rry 20 c++ g++ language-lawyer standard-layout clang++
大卫霍尔曼最近在推特上发布了以下示例(我稍微减少了):
struct FooBeforeBase {
double d;
bool b[4];
};
struct FooBefore : FooBeforeBase {
float value;
};
static_assert(sizeof(FooBefore) > 16);
//----------------------------------------------------
struct FooAfterBase {
protected:
double d;
public:
bool b[4];
};
struct FooAfter : FooAfterBase {
float value;
};
static_assert(sizeof(FooAfter) == 16);
Run Code Online (Sandbox Code Playgroud)
您可以检查godbolt上的clang中的布局,并查看大小更改的原因是,FooBefore成员value放置在偏移16处(保持完全对齐8 FooBeforeBase)而在FooAfter,成员value放置在偏移12处(有效地使用FooAfterBase的尾巴填充).
我很清楚这FooBeforeBase是标准布局,但FooAfterBase不是(因为它的非静态数据成员并不都具有相同的访问控制,[class.prop]/3).但是FooBeforeBase,标准布局需要这方面的填充字节是什么呢?
gcc和clang都重用了FooAfterBase填充,最后用了sizeof(FooAfter) == 16.但是MSVC没有,结果是24.每个标准是否有必要的布局,如果没有,为什么gcc和clang做他们做的事情?
有一些混乱,所以只是为了清理:
FooBeforeBase 是标准布局FooBefore是不(包括它和基类具有非静态数据成员,类似于E在该实施例中)FooAfterBase是不(它具有不同的访问非静态数据成员)FooAfter是不是(由于上述两个原因)这个问题的答案不是来自标准,而是来自Itanium ABI(这就是为什么gcc和clang有一种行为,但msvc做了别的事情).ABI定义了一个布局,为了这个问题,其相关部分是:
对于规范内部的目的,我们还指定:
- dsize(O):对象的数据大小,是没有尾部填充的O的大小.
和
我们忽略了POD的尾部填充,因为标准的早期版本不允许我们将其用于其他任何东西,因为它有时允许更快地复制该类型.
将虚拟基类以外的成员放置定义为:
从偏移量dsize(C)开始,如果需要,则增加以对齐基类的nvalign(D)或对齐数据成员(D).除非[...不相关...],否则将D放在此偏移处.
术语POD已从C++标准中消失,但它意味着标准布局和平凡的可复制.在这个问题中,FooBeforeBase是一个POD.Itanium ABI忽略尾部填充 - 因此dsize(FooBeforeBase)是16.
但FooAfterBase它不是POD(它可以轻易复制,但它不是标准布局).因此,尾部填充不会被忽略,因此dsize(FooAfterBase)只有12,并且float可以直接在那里.
这有一些有趣的结果,正如Quuxplusone在相关答案中所指出的,实现者通常也认为尾部填充不会被重用,这会对这个例子造成严重破坏:
Run Code Online (Sandbox Code Playgroud)#include <algorithm> #include <stdio.h> struct A { int m_a; }; struct B : A { int m_b1; char m_b2; }; struct C : B { short m_c; }; int main() { C c1 { 1, 2, 3, 4 }; B& b1 = c1; B b2 { 5, 6, 7 }; printf("before operator=: %d\n", int(c1.m_c)); // 4 b1 = b2; printf("after operator=: %d\n", int(c1.m_c)); // 4 printf("before std::copy: %d\n", int(c1.m_c)); // 4 std::copy(&b2, &b2 + 1, &b1); printf("after std::copy: %d\n", int(c1.m_c)); // 64, or 0, or anything but 4 }
这=是正确的事情(它没有覆盖B的尾部填充),但copy()有一个库优化减少到memmove()- 它不关心尾部填充,因为它假定它不存在.