大卫·霍尔曼(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);
您可以检查Godbolt上clang中的布局,并看到大小更改的原因是,在FooBefore
中,成员value
放置在偏移16处(保持FooBeforeBase
的8个完全对齐),而在FooAfter
中,成员value
放置在偏移12(有效使用FooAfterBase
的尾板)。
对我来说很明显FooBeforeBase
是标准的,但是FooAfterBase
不是(因为其非静态数据成员并非都具有相同的访问控件,[class.prop]/3)。但是,FooBeforeBase
的标准层需要这种尊重字节的尊重呢?
gcc和clang reuse FooAfterBase
的填充物,最终以 sizeof(FooAfter) == 16
结束。但是MSVC不会以24结束。是否根据标准有必要的布局,如果没有,GCC和Clang为什么要做他们的工作?
有一些混乱,所以只是要清除:
-
FooBeforeBase
是标准layout -
FooBefore
是不是(IT和基类都有非静态数据成员,类似于此示例中的E
) -
FooAfterBase
是不是(它具有不同访问的非静态数据成员) -
FooAfter
是不是(出于上述两个原因)
这个问题的答案不是来自标准,而是来自Itanium abi(这就是为什么GCC和Clang具有一种行为,但MSVC确实如此其他)。ABI定义了一个布局,就此问题而言,其相关部分是:
出于规范内部的目的,我们还指定:
- dsize (o):对象的数据大小,它是没有尾巴填充的O的大小。
和
我们忽略了豆荚的尾填充,因为标准的早期版本不允许我们将其用于其他任何东西,并且有时可以更快地复制该类型。
在虚拟基类以外的其他成员的位置定义为:
从偏移dsize(c)开始,必要时会增加与基础类别的nValign(d)或对数据成员的对齐(d)。将D放在此偏移量中,除非[...不相关...]。
术语pod已从C 标准中消失,但表示标准层,并且可以复制。在这个问题中,FooBeforeBase
是一个豆荚。Itanium abi忽略了尾填充 - 因此dsize(FooBeforeBase)
为16。
,但FooAfterBase
不是POD(它是可以毫无复制的,但它不是不是标准layout)。结果,不忽略尾垫,因此dsize(FooAfterBase)
仅为12,float
可以就在那里。
这会带来有趣的后果,正如Quuxplusone在相关答案中指出的那样,实施者通常还假定尾垫没有再使用,这在此示例上造成了严重破坏:
#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=: %dn", int(c1.m_c)); // 4 b1 = b2; printf("after operator=: %dn", int(c1.m_c)); // 4 printf("before std::copy: %dn", int(c1.m_c)); // 4 std::copy(&b2, &b2 + 1, &b1); printf("after std::copy: %dn", int(c1.m_c)); // 64, or 0, or anything but 4 }
在这里,=
做正确的事情(它不会覆盖B
的尾填充),但是copy()
具有库的优化,可以减少到memmove()
-它不在乎尾垫,因为它假设它不存在。
FooBefore derived;
FooBeforeBase src, &dst=derived;
....
memcpy(&dst, &src, sizeof(dst));
如果将附加的数据成员放在孔中,则memcpy
会覆盖它。
在注释中正确指出的是,该标准不需要此memcpy
调用应起作用。但是,Itanium Abi似乎是在设计这种情况的。也许对ABI规则进行了这种方式,以使混合语言编程更加健壮,或者保留某种向后兼容。
可以在此处找到相关的ABI规则。
这里可以找到一个相关的答案(这个问题可能是该问题的重复)。
这是一个具体案例,说明了为什么为什么第二种情况无法重复使用填充:
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) );
这是未定义的行为。
FooBefore
也不是std-layout;两个类声明了无静态数据成员(FooBefore
和FooBeforeBase
)。因此,允许编译器任意放置一些数据成员。因此,出现了不同工具链的差异。在STD-Layout层次结构中,在最多的类(最多派生的类或中间类)应声明无静态数据成员。
这与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进行。