结构由编译器重新排序

Dar*_*bik 34 c c++ struct memory-alignment

假设我有这样的结构:

struct MyStruct
{
  uint8_t var0;
  uint32_t var1;
  uint8_t var2;
  uint8_t var3;
  uint8_t var4;
};
Run Code Online (Sandbox Code Playgroud)

这可能会浪费很多(不是很多)空间.这是因为uint32_t变量的必要对齐.

实际上(在对齐结构以便它实际上可以使用uint32_t变量之后)它可能看起来像这样:

struct MyStruct
{
  uint8_t var0;
  uint8_t unused[3];  //3 bytes of wasted space
  uint32_t var1;
  uint8_t var2;
  uint8_t var3;
  uint8_t var4;
};
Run Code Online (Sandbox Code Playgroud)

更有效的结构将是:

struct MyStruct
{
  uint8_t var0;
  uint8_t var2;
  uint8_t var3;
  uint8_t var4;
  uint32_t var1;
};
Run Code Online (Sandbox Code Playgroud)

现在,问题是:

为什么编译器禁止(按标准)重新排序结构?

如果对结构进行重新排序,我认为你没有任何方法可以用脚射击自己.

Mat*_* M. 36

为什么编译器禁止(按标准)重新排序结构?

基本原因是:与C兼容.

请记住,C最初是一种高级汇编语言.在C中通过将字节重新解释为特定的来查看内存(网络数据包......)是很常见的struct.

这导致依赖于此属性的多个功能:

  • C保证a struct的地址和第一个数据成员的地址是同一个,所以C++也是如此(在没有virtual继承/方法的情况下).

  • C保证如果你有两个struct A并且B都以数据成员char后跟数据成员int(以及之后的任何内容)开始,那么当你将它们放入a时union你可以编写B成员并读取charint通过其A成员,所以C++也是如此:标准布局.

后者非常广泛,完全可以防止大多数struct(或class)数据成员的重新排序.


请注意,标准允许一些重新排序:由于C没有访问控制的概念,C++指定不指定具有不同访问控制说明符的两个数据成员的相对顺序.

据我所知,没有编译器试图利用它; 但他们理论上可以.

在C++之外,诸如Rust之类的语言允许编译器重新排序字段,并且主要的Rust编译器(rustc)默认情况下这样做.只有历史决策和强烈的向后兼容性才能阻止C++这样做.

  • 这里有一些好处.提醒我[如果更改`-std`标志的值,[编译运行之间的排序可能不同](http://stackoverflow.com/a/15763092/560648);) (4认同)

Lig*_*ica 28

如果结构被重新排序,我认为你没有任何方法可以用脚射击自己.

真?如果这是允许的,默认情况下,即使在相同的过程中,库/模块之间的通信也是非常危险的.

"在宇宙中"的论点

我们必须能够知道我们的结构是按照我们要求它们的方式来定义的.填充是未指定的,这已经够糟糕了!幸运的是,您可以在需要时控制它.

好的,从理论上讲,可以制作一种新语言,类似地,除非给出某些属性,否则成员可以重新订购.毕竟,我们不应该在对象上做内存级魔术,所以如果只使用C++习语,那么默认情况下你是安全的.

但这不是我们生活的实际现实.


"走出宇宙"的说法

如果用你的话说"每次都使用相同的重新排序",你就可以把事情安全起来.该语言必须明确说明如何订购成员.标准写作复杂,理解复杂,实施复杂.

只是保证订单与代码一样容易,并将这些决定留给程序员.请记住,这些规则源于旧C,旧C赋予程序员权力.

您已经在您的问题中展示了通过简单的代码更改使结构填充有效是多么容易.在语言级别上不需要为您执行此操作增加任何复杂性.

  • 图书馆/模块*之间的通信*在同一过程中*将是非常危险的. (7认同)
  • @DarthRubik:你如何每次使用相同的订单强制执行每个编译器的每次运行?哦,这是正确的,通过留下程序员写的它大声笑 (5认同)
  • @Revolver_Ocelot作为其ABI的一部分,平台可以指定一种简单的,确定性的重新排序方案,该方案以最小的成本获得了大部分的包装效益.例如,只需按大小(最大的第一个)稳定排序对象即可. (3认同)
  • 如果每次都使用相同的重新排序..... (2认同)
  • 语言不必指定填充或跨模块兼容的顺序; 这由ABI处理,就像函数调用一样. (2认同)

Lun*_*din 14

该标准保证分配顺序仅仅因为结构可以表示某种存储器布局,例如数据协议或硬件寄存器的集合.例如,程序员和编译器都不能自由地重新排列TPC/IP协议中的字节顺序或微控制器的硬件寄存器.

如果订单不能得到保证,那structs将是纯粹的抽象数据容器(类似于C++向量),我们不能假设它们,除了它们以某种方式包含我们放在其中的数据.在进行任何形式的低级编程时,它会使它们变得更加无用.


Sha*_*olf 7

如果结构被其他编译器或其他语言生成的任何其他低级代码读取,编译器应保持其成员的顺序.假设您正在创建一个操作系统,并且您决定在C中编写部分操作系统,并在部署中编写部分操作系统.您可以定义以下结构:

struct keyboard_input
{
    uint8_t modifiers;
    uint32_t scancode;
}
Run Code Online (Sandbox Code Playgroud)

您将其传递给程序集例程,您需要手动指定结构的内存布局.您希望能够在具有4字节对齐的系统上编写以下代码.

; The memory location of the structure is located in ebx in this example
mov al, [ebx]
mov edx, [ebx+4]
Run Code Online (Sandbox Code Playgroud)

现在说编译器会以实现定义的方式更改结构中成员的顺序,这意味着根据您使用的编译器和传递给它的标志,您可能最终得到扫描码的第一个字节al中的成员,或者使用修饰符成员.

当然,问题不仅仅是简化为具有汇编例程的低级接口,而且如果使用不同编译器构建的库会相互调用(例如,使用windows API构建带有mingw的程序),也会出现问题.

因此,语言只会迫使您考虑结构布局.

  • 因此系统具有4字节对齐.它将是一个系统,其中数据结构的所有成员都被填充以在4字节边界上开始,这在32位系统上相当常见. (3认同)

Zal*_*nGG 5

请记住,不仅自动重新排序元素以改善打包可能会损害特定的内存布局或二进制序列化,但程序员可能已经仔细选择了属性的顺序,以利用常用成员的缓存局部性更少访问.


sup*_*cat 5

Dennis Ritchie 设计的语言不是根据行为而是根据内存布局来定义结构的语义。如果结构 S 在偏移量 X 处有一个类型为 T 的成员 M,则 MS 的行为被定义为获取 S 的地址,向其添加 X 个字节,将其解释为指向 T 的指针,并将由此标识的存储解释为一个左值。写入结构成员将更改其关联存储的内容,更改成员存储的内容将更改成员的值。代码可以自由地使用多种方法来操作与结构成员关联的存储,并且语义将根据对该存储的操作来定义。

代码可以操作与结构相关联的存储的有用方法之一是使用 memcpy() 将一个结构的任意部分复制到另一个结构的相应部分,或使用 memset() 清除结构的任意部分。由于结构成员按顺序排列,因此可以使用单个 memcpy() 或 memset() 调用复制或清除一系列成员。

标准委员会定义的语言在许多情况下消除了对结构成员的更改必须影响底层存储或对存储的更改影响成员值的要求,这使得对结构布局的保证不如在 Ritchie 的语言中有用。尽管如此,仍保留了使用 memcpy() 和 memset() 的能力,并且保留该能力需要保持结构元素的顺序。