David Hollman最近在推文中发布了以下示例(我略微简化了该示例):
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);
您可以检查布局in clang on godbolt并发现大小更改的原因是在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
不是 (它和基类都具有非静态数据成员,类似于this example中的E
)FooAfterBase
不是 (它具有具有不同访问权限的非静态数据成员)FooAfter
不是 答案 0 :(得分:9)
这个问题的答案不是来自标准,而是来自Itanium ABI(这就是为什么gcc和clang具有一种行为,而msvc具有其他行为的原因)。 ABI定义了a layout,出于该问题的目的,其中的相关部分是:
出于规范内部的目的,我们还指定:
- dsize (O):对象的数据大小,即没有尾部填充的O的大小。
和
我们忽略POD的尾部填充,因为该标准的早期版本不允许我们将其用于其他任何东西,并且因为它有时允许更快地复制该类型。
除虚拟基类以外的其他成员的位置定义为:
从偏移量dsize(C)开始,如果有必要,则增加该偏移量以将其对齐到基类的nvalign(D)或数据成员的align(D)。除非[...不相关...],否则将D放置在此偏移量上。
POD术语已从C ++标准中消失,但它表示标准版式且可轻松复制。在此问题中,FooBeforeBase
是POD。 Itanium ABI忽略尾部填充-因此dsize(FooBeforeBase)
为16。
但是FooAfterBase
不是POD(它可以普通复制,但不是标准布局)。结果,尾部填充不被忽略,因此dsize(FooAfterBase)
仅12,而float
可以直接到那里。
正如Quuxplusone在related answer中指出的那样,这会产生有趣的结果,实现者通常还假设尾部填充没有被重用,这给该示例造成了严重破坏:
#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()
-并不关心尾部填充,因为它假定它不存在。
答案 1 :(得分:1)
FooBefore derived;
FooBeforeBase src, &dst=derived;
....
memcpy(&dst, &src, sizeof(dst));
如果将其他数据成员放在孔中,则memcpy
将覆盖它。
正如注释中正确指出的那样,该标准并不要求此memcpy
调用可以正常工作。但是,Itanium ABI似乎是针对这种情况而设计的。也许以此方式指定了ABI规则,以使混合语言编程更加健壮,或保留某种向后兼容性。
可以在here中找到相关的ABI规则。
可以找到一个相关的答案here(此问题可能是该问题的重复)。
答案 2 :(得分:-1)
这是一个具体案例,说明了为什么第二种案例无法重复使用填充:
union bob {
FooBeforeBase a;
FooBefore b;
};
bob.b.value = 3.14;
memset( &bob.a, 0, sizeof(bob.a) );
这无法清除bob.b.value
。
union bob2 {
FooAfterBase a;
FooAfter b;
};
bob2.b.value = 3.14;
memset( &bob2.a, 0, sizeof(bob2.a) );
这是未定义的行为。
答案 3 :(得分:-1)
sortIndices
也不是标准布局;有两个类声明了非静态数据成员(FooBefore
和FooBefore
)。因此,允许编译器任意放置一些数据成员。因此,在不同的工具链上会出现差异。
在标准布局层次结构中,至少一个类(最派生的类或最多一个中间类)应声明非静态数据成员。
答案 4 :(得分:-2)
这与n.m.的答案类似。
首先,让我们有一个清除FooBeforeBase
的函数:
void clearBase(FooBeforeBase *f) {
memset(f, 0, sizeof(*f));
}
这很好,因为clearBase
指向FooBeforeBase
的指针,它认为FooBeforeBase
具有标准布局,因此进行记忆是安全的。
现在,如果您这样做:
FooBefore b;
b.value = 42;
clearBase(&b);
您不希望clearBase
会清除b.value
,因为b.value
不属于FooBeforeBase
。但是,如果将FooBefore::value
放在FooBeforeBase
的尾部填充中,则也将被清除。
根据标准,是否有必需的布局?如果没有,为什么gcc和clang会做它们的工作?
否,不需要尾巴填充。这是gcc和clang所做的优化。