确定C/C++结构体相对于其成员的对齐方式

28

如果已知结构体成员的对齐方式,能否找到结构体类型的对齐方式?

例如:

struct S
{
 a_t a;
 b_t b;
 c_t c[];
};

S = max(alignment_of(a), alignment_of(b), alignment_of(c))的对齐方式是什么?

在搜索互联网时,我发现“对于结构化类型,其元素中最大的对齐要求确定了结构的对齐方式”(来自每个程序员都应该知道的内存知识),但我在标准中(确切地说是最新草案)没有找到任何类似的内容。


编辑: 非常感谢所有的答案,特别是Robert Gamble,他为原始问题提供了一个非常好的答案,以及其他贡献者。

简而言之:

为了确保结构成员的对齐要求,结构的对齐方式必须至少与其最严格的成员的对齐方式一样严格。

至于确定结构的对齐方式,提出了几个选项,并经过一些研究,这就是我找到的内容:

  • c++ std::tr1::alignment_of
    • 目前还不是标准,但已接近(技术报告1),应该在C++0x中出现
    • 最新草案中存在以下限制:前提条件:T必须是完整类型、引用类型或未知大小的数组,但不能是函数类型或(可能带有cv限定符的)void。
      • 这意味着我提出的使用C99柔性数组的用例将无法工作(这并不奇怪,因为柔性数组不是标准c++)
    • 在最新的c++草案中,它是用一个新的关键字alignas来定义的(具有相同的完整类型要求)
    • 在我看来,如果c++标准支持C99柔性数组,要求可以放宽(具有柔性数组的结构的对齐方式不应根据数组元素的数量而改变)
  • c++ boost::alignment_of
    • 主要是tr1的替代品
    • 似乎专门针对void并返回0(这在c++草案中是禁止的)
    • 开发人员的注意事项:严格来说,您只应依赖于ALIGNOF(T)的值是T的真实对齐方式的倍数,尽管在我们所知道的所有情况下,它确实计算了正确的值。
    • 我不知道这是否适用于柔性数组,应该可以(可能不适用于一般情况,在我的平台上,这将解析为编译器内部函数,因此我不知道它在一般情况下的行为如何)
  • Andrew Top提供了一个简单的模板解决方案来计算对齐方式
    • 这似乎非常接近boost所做的(如果对象大小小于计算出的对齐方式,boost还将返回对象大小作为对齐方式),因此可能适用相同的注意事项
    • 这适用于柔性数组
  • 使用Windbg.exe查找符号的对齐方式
    • 不是编译时,与编译器有关,未测试
  • 在包含类型的匿名结构上使用offsetof
    • 请参阅答案,不可靠,在c++非POD中不可移植
  • 编译器内部函数,例如MSVC __alignof
    • 适用于柔性数组
    • alignof关键字在最新的c++草案中

如果我们想使用“标准”解决方案,我们只能使用std :: tr1 :: alignment_of,但如果将c ++代码与c99的灵活数组混合使用,则无法正常工作。
在我看来,只有一个解决方案-使用旧的结构体技巧:
struct S
{
 a_t a;
 b_t b;
 c_t c[1]; // "has" more than 1 member, strictly speaking this is undefined behavior in both c and c++ when used this way
};
对于这种情况(以及其他所有情况),c和c++标准的分歧和日益增长的差异非常不幸。

另一个有趣的问题是(如果我们无法以可移植的方式找出结构的对齐方式),最严格的对齐要求是什么。我能找到一些解决方案:
- boost(内部)使用各种类型的联合,并在其上使用boost::alignment_of - 最新的C++草案包含std::aligned_storage - 默认对齐值应为大小不大于Len的任何C++对象类型的最严格对齐要求
- 所以std::alignment_of< std::aligned_storage<BigEnoughNumber>>::value应该给我们最大的对齐方式 - 草案而非标准(如果有的话),tr1::aligned_storage没有这个属性
任何关于此的想法也将不胜感激。
我暂时取消了采纳的答案,以便更多地关注并获得有关新的子问题的输入。

底线是只有编译器知道在编译时如何对齐对象,因此获取此信息的唯一方法是从编译器获取。在此标准化之前,您需要使用编译器扩展。您需要此信息做什么? - Robert Gamble
虚拟机(需要对象、数组、垃圾回收器和许多其他杂项的低级构造)。这是一个学生项目,应尽可能“可移植”和“符合标准”。 - Hrvoje Prgeša
让这个工作起来并不是问题(而且大部分已经完成了),但我没有时间在多个平台和编译器上测试项目,因此我正在寻找一些符合标准的解决方案来解决我面临的问题。 - Hrvoje Prgeša
我知道这在标准方面很难做到。我总结了这些信息,希望对其他寻找相同内容的人有所帮助。(评论的300个字符限制真是荒谬) - Hrvoje Prgeša
就标准而言,我也对最新的C草案中几乎没有提到对齐感到失望 - 几年后,带有alignof和align关键字的C++可能会比C更加“低级” :) - Hrvoje Prgeša
10个回答

28
这里涉及到两个紧密相关的概念:
  1. 处理器访问特定对象需要的对齐方式
  2. 编译器在内存中放置对象实际使用的对齐方式
为了确保结构成员的对齐要求,结构的对齐方式必须至少与其最严格成员的对齐方式一样严格。虽然标准没有明确说明这一点,但可以从以下事实推断出来(标准单独说明了这些事实):
  • 结构体成员之间(以及末尾)允许有填充
  • 数组元素之间不允许有填充
  • 您可以创建任何结构类型的数组
如果结构的对齐方式不至少与其每个成员的对齐方式一样严格,则无法创建结构数组,因为某些结构成员或某些元素将没有正确对齐。
现在编译器必须根据其成员的对齐要求来确保结构的最小对齐方式,但它也可以以比所需更严格的方式对齐对象,这通常是为了提高性能而做的。例如,许多现代处理器将允许以任何对齐方式访问32位整数,但如果它们未对齐在4字节边界上,则访问可能会显着变慢。
没有一种可移植的方法可以确定处理器对任何给定类型执行的对齐方式,因为这不是语言公开的。尽管如此,由于编译器显然了解目标处理器的对齐要求,因此它可以将此信息作为扩展公开。
在C中也没有一种可移植的方式来确定编译器实际上将如何对其进行对齐,尽管许多编译器有选项来提供一定程度的对齐控制。

我相信你是对的。但是我发现很难从这三个事实中得出逻辑结论。你能概述一下主要步骤吗? - Johannes Schaub - litb
你的回答很好,我想我明白了 :) - Johannes Schaub - litb
关于确定对齐方式,c++ std::tr1::alignment_of<S>::value 怎么样? - Hrvoje Prgeša
我不了解alignment_of(我是一名C程序员,对C ++不太了解),那是标准吗?你能提供一个链接吗? - Robert Gamble
我对第二点不确定。假设在一个需要将整数绑定到4字节边界的32位架构中,有一个结构体S { int a; char b; }。当我们定义一个S类型的数组时,相邻两个元素之间肯定会有填充,是吗? - David Rodríguez - dribeas
显示剩余8条评论

15

我编写了这个类型特征代码,用于确定任何类型的对齐方式(基于已经讨论过的编译器规则)。你可能会觉得它有用:

template <class T>
class Traits
{
public:
    struct AlignmentFinder
    {
        char a; 
        T b;
    };

    enum {AlignmentOf = sizeof(AlignmentFinder) - sizeof(T)};
};

现在你可以执行以下操作:

std::cout << "The alignment of structure S is: " << Traits<S>::AlignmentOf << std::endl;

这应该相当于std :: tr1 :: alignment_of <T> :: value,尽管它是一个非常干净和简单的实现。 - Hrvoje Prgeša
不知道 std::tr1::alignment_of<T> 这个东西... 真遗憾,我也搜索了一下,什么都没找到。感谢你提到它。 - Andrew Top

7
下面这个宏将返回任何给定类型(即使它是一个结构体)的对齐要求:
#define TYPE_ALIGNMENT( t ) offsetof( struct { char x; t test; }, test )

注意:我可能从我的过去某个时期的Microsoft标题中借用了这个想法...
编辑:正如Robert Gamble在评论中指出的那样,这个宏并不保证可行。事实上,如果编译器设置为打包结构中的元素,它肯定不能很好地工作。因此,如果你决定使用它,请小心使用。
一些编译器有一个扩展,允许你获取类型的对齐方式(例如,从VS2002开始,MSVC有一个__alignof()内置函数)。当可用时应该使用它们。

1
这将为您提供编译器在给定结构中选择用于类型t的对齐方式,但是当该类型t不出现在这样的结构中时,这可能不是类型t的对齐方式。 - Robert Gamble
1
我承认标准可能无法保证该宏起作用,因此应使用#if预处理器控制来保护它,以便为特定的编译器正确定义。尽管如此,实践中似乎它确实很好地工作。 - Michael Burr
1
另外,据我回忆,offsetof 仅保证对 PODs 起作用,因此如果 t 是非 POD,则再次超出标准范围。 - Hrvoje Prgeša

3
如果您了解正在使用的编译器选项的更多详细信息,那么可以假定结构对齐是可能的。例如,#pragma pack(1)会强制某些编译器在字节层面上对齐。
顺便说一句:我知道问题是关于对齐的,但一个次要问题是填充。对于嵌入式编程、二进制数据等等,在一般情况下,如果可能的话,不要假定任何关于结构对齐的东西。如果必要,可以在结构中使用显式填充。我曾经遇到过这样的情况:在不同平台的编译器上,无法复制使用一个编译器内部的结构的确切对齐方式,而添加填充元素则可以解决这个问题。原因是结构内的结构的对齐。

我认为你混淆了对齐和填充,它们是相关但不同的概念。填充用于确保正确的对齐,这就是pack命令处理的内容;如果您需要在结构体中使用显式填充,则几乎肯定访问其代码存在问题。 - Robert Gamble
如果你编写嵌入式设备或二进制通信程序,你经常会处理对齐和填充。虽然问题是关于对齐的,但我想提一下填充,以防它成为一个附带问题。 - Ryan
我也很好奇你的话中,“……如果你需要在你的结构中使用显式填充……”是否有编写嵌入式设备和跨平台/编译器的经验?你看过Linux内核代码吗?有时这是绝对必要的。 - Ryan
@Robert Gamble:“代码几乎肯定有问题”并非一定如此。如果您想确保两个元素不出现在同一个缓存行上,以防止多核处理器上的乒乓现象,您需要手动添加正确数量的填充元素。 - oz10

3
正如其他人所提到的,它的实现取决于具体情况。Visual Studio 2005默认使用8字节作为结构体对齐方式。在内部,项目按其大小对齐 - 浮点数具有4字节对齐,双精度使用8字节等。
您可以使用#pragma pack覆盖此行为。GCC(以及大多数编译器)具有类似的编译器选项或#pragma。

2

如果您想在Windows的特定情况下找到这个问题,打开windbg:

Windbg.exe -z \path\to\somemodule.dll -y \path\to\symbols

接下来,运行:

dt somemodule!CSomeType

1
我在8年后阅读了这个答案,感觉@Robert的被采纳的答案大体上是正确的,但在数学上是错误的。
为了确保结构成员的对齐要求,结构的对齐必须至少与其成员的对齐的最小公倍数一样严格。考虑一个奇怪的例子,其中成员的对齐要求为4和10;在这种情况下,结构的对齐是LCM(4, 10),即20,而不是10。当然,看到具有不是2的幂次方的对齐要求的平台是很奇怪的,因此在所有实际情况下,结构对齐等于其成员的最大对齐。
原因是只有当结构的地址以其成员对齐的LCM开头时,才能满足所有成员的对齐,并且成员之间的填充和结构末尾的填充与起始地址无关。 更新:正如@chqrlie在评论中指出的那样,C标准不允许对齐的奇数值。但是,这个答案仍然证明了为什么结构对齐是其成员对齐的最大值,因为最大值恰好是最小公倍数,因此成员始终相对于公共倍数地址对齐。

如果对齐值始终是2的幂,则我将非常奇怪且不符合规范:*6.2.8 对象的对齐方式 [...] 4 [...] 每个有效的对齐值都应该是非负整数的2的幂。 - chqrlie
感谢指出标准不允许对齐值为非2的幂。这个规定是在C11中添加的还是在C99中就已经存在了呢?我曾在一些平台上看到过80位浮点数,当然它的对齐要求仍然可能是2的幂。 - user1969104

1

我认为在任何C标准中都不能保证内存布局。这非常依赖于供应商和架构。可能有一些方法可以在90%的情况下工作,但它们不是标准的。

虽然我很乐意被证明是错误的 =)


标准实际上提供了多个保证,包括:1)在结构体的第一个成员之前没有填充;2)结构体成员按照定义的顺序在内存中排列;3)数组的成员之间没有填充。 - Robert Gamble
将这些保证与所有结构都可以用作数组元素的事实相结合,可以得出结构的对齐必须至少与其最严格成员的对齐一样严格。 - Robert Gamble
我知道我们在谈论C语言,但是为了明确起见,C++在某些情况下放宽了结构体成员按定义顺序排列的要求。 - Michael Burr
@Mike B 没错,但如果类型是POD,C规则仍然适用。 - Hrvoje Prgeša

1

我基本上同意Paul Betts、Ryan和Dan的观点。实际上,这取决于开发人员,您可以保留Robert提到的默认对齐语义(Robert的解释只是默认行为,并不是强制或必需的),或者您可以设置任何您想要的对齐方式/Zp[##]。

这意味着,如果您有一个包含浮点数、长双精度、uchar等各种数组的typedef。然后再有另一种类型,其中包含一些这些奇形怪状的成员和一个字节,然后是另一个奇怪的成员,它将简单地按照制造/解决方案文件定义的任何首选项进行对齐。

如前所述,在运行时使用windbg的dt命令,您可以找出编译器在内存中如何布置结构。

您还可以使用任何pdb阅读工具,例如dia2dump从pdb静态地提取此信息。


1

修改自{{link1:Peeter Joot的博客}}

C结构对齐是基于结构中最大的本地类型,至少通常是这样(一个例外是在win32上使用64位整数,只需要32位对齐)。

如果您只有字符和字符数组,一旦添加了int,该int将以4字节边界开始(可能会在int成员之前添加隐藏填充)。此外,如果结构体不是sizeof(int)的倍数,则会在末尾添加隐藏填充。短类型和64位类型也是如此。

例如:

struct blah1 {
    char x ;
    char y[2] ;
};

sizeof(blah1) == 3

struct blah1plusShort {
    char x ;
    char y[2] ;
    // <<< hidden one byte inserted by the compiler here
    // <<< z will start on a 2 byte boundary (if beginning of struct is aligned).
    short z ;
    char w ;
    // <<< hidden one byte tail pad inserted by the compiler.
    // <<< the total struct size is a multiple of the biggest element.
    // <<< This ensures alignment if used in an array.
};

sizeof(blah1plusShort) == 8


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接