标准布局和尾部填充

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不是(由于上述两个原因)

Bar*_*rry 9

这个问题的答案不是来自标准,而是来自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在相关答案中所指出的,实现者通常也认为尾部填充不会被重用,这会对这个例子造成严重破坏:

#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
}
Run Code Online (Sandbox Code Playgroud)

=是正确的事情(它没有覆盖B的尾部填充),但copy()有一个库优化减少到memmove()- 它不关心尾部填充,因为它假定它不存在.