标准布局和尾部填充

时间:2018-12-18 16:32:07

标签: c++ g++ language-lawyer clang++ standard-layout

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不是

5 个答案:

答案 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也不是标准布局;有两个类声明了非静态数据成员(FooBeforeFooBefore)。因此,允许编译器任意放置一些数据成员。因此,在不同的工具链上会出现差异。 在标准布局层次结构中,至少一个类(最派生的类或最多一个中间类)应声明非静态数据成员。

答案 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所做的优化。