【问题标题】:Why does this struct padding trick work?为什么这个结构填充技巧有效?
【发布时间】:2015-09-24 07:17:45
【问题描述】:

考虑这个简单的程序

#include <iostream>

struct A
{
    int   x1234;
    short x56;
    char  x7;
};

struct B : A
{
    char x8;
};

int main()
{
    std::cout << sizeof(A) << ' ' << sizeof(B) << '\n';
    return 0;
}

这打印出8 12。尽管B 可以在不破坏对齐要求的情况下打包成 8 个字节,但它却占用了 12 个字节。

如果有sizeof(B) == 8 会很好,但答案是 Is the size of a struct required to be an exact multiple of the alignment of that struct? 表示没有办法。

因此,当以下情况发生时,我感到很惊讶

struct MakePackable
{
};

struct A : MakePackable
{
    int   x1234;
    short x56;
    char  x7;
};

struct B : A
{
    char x8;
};

打印8 8

这里发生了什么?我怀疑标准布局类型与它有关。如果是这样,那么导致上述行为的原因是什么,而该功能的唯一目的是确保与 C 的二进制兼容性?


编辑:正如其他人指出的那样,这是 ABI 或编译器特定的,所以我应该补充一点,在使用以下编译器的 x86_64-unknown-linux-gnu 上观察到了这种行为:

  • clang 3.6
  • gcc 5.1

我还从 clang 的 struct dumper 中发现了一些奇怪的东西。如果我们要求没有尾部填充(“dsize”)的数据大小,

          A   B
first     8   9
second    7   8

那么在第一个例子中我们得到dsize(A) == 8。为什么不是 7?

【问题讨论】:

  • 也许除了编译器实现细节之外没有其他理由......
  • 您将不得不询问特定的 ABI,因为这种行为在 C++ 本身范围内的可能性几乎为零。 FWIW,我在 Itanium 中找不到任何东西(一目了然)来解释这一点,尽管我确实在 GCC 5.1 中得到了8 8,所以...:/
  • 标签language-lawyer适合这个问题吗?
  • 我得到8 12 用于VS2013下的第二个代码sn-p,它似乎是特定于编译器实现的。
  • 这是特定于您的编译器的。一个非常相似的案例是described here

标签: c++ inheritance padding


【解决方案1】:

我不是真正的 C++ 语言律师,但到目前为止我发现的是:

参考this question 中的答案,结构仅保留标准布局 POD,而其自身及其父类之间只有 1 个具有非静态成员的类。所以在这个想法下,A 在这两种情况下都有保证的布局,但B 在任何一种情况下都没有。

支持这一点的事实是,std::is_podA 为真,B 为假。

因此,如果我自己正确理解了这一点,那么在这两种情况下,编译器都可以通过 B 的布局来做它想做的事情。显然,在第二种情况下,感觉就像在利用 A 的填充字节。

【讨论】:

  • 一个标准布局类:"要么在最派生的类中没有非静态数据成员,并且最多有一个具有非静态数据成员的基类,要么没有具有非静态数据成员的基类-静态数据成员” [C++14: 9/7]
  • @LightnessRacesinOrbit 措辞更改为this proposal,现在对我来说没有意义。
【解决方案2】:

这是一个数据点,虽然不是完整的答案。

假设我们有(作为一个完整的翻译单元,而不是一个 sn-p):

struct X {};

struct A
{
    int   x1234;
    short x56;
    char  x7;
}

void func(A &dst, A const &src) 
{
    dst = src;
}

使用g++,这个函数被编译成:

movq    (%rdx), %rax
movq    %rax, (%rcx)

但是如果用struct A : X代替,那么这个函数是:

movl    (%rdx), %eax
movl    %eax, (%rcx)
movzwl  4(%rdx), %eax
movw    %ax, 4(%rcx)
movzbl  6(%rdx), %eax
movb    %al, 6(%rcx)

这两种情况实际上分别对应于OP示例中的8 128 8

原因很清楚:A 可能被用作某个类B 的基础,然后调用func(b, a); 必须小心不要打扰可能驻留在其中的b 的其他成员填充区域(在 OP 的示例中为b.x8);

我在 C++ 标准中看不到 A : X 的任何特定属性,这会使 g++ 决定填充可在 struct A : X 中重复使用,但在 struct A 中不可用。 AA : X 都是可简单复制标准布局POD

我想这一定只是基于典型用法的优化决策。没有重复使用的版本会更快复制。也许 g++ ABI 设计师可以发表评论?

有趣的是,这个例子表明可简单复制并不意味着memcpy(&amp;b, &amp;a, sizeof b) 等同于b = a

【讨论】:

  • 我猜这与 C 兼容性有关。虽然 C++ 程序必须知道任何非最终类型都可能派生自,因此数据成员放置在填充中,但没有 C 程序需要这种假设。当您从某种类型派生 A 时,您不能在 C 中使用完全相同的 struct
  • @dyp 说得通
猜你喜欢
  • 2021-05-18
  • 1970-01-01
  • 1970-01-01
  • 2010-09-11
  • 2014-10-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多