标准层和尾填充



大卫·霍尔曼(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;两个类声明了无静态数据成员(FooBeforeFooBeforeBase)。因此,允许编译器任意放置一些数据成员。因此,出现了不同工具链的差异。在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进行。

最新更新