结构体联合体共享相同第一成员

7

我不确定代码中是否存在指针别名(或其他标准符合性问题)在断言转换中。似乎一个指向联合类型的指针可以被强制转换为第一个成员的指针,由于联合体仅由这两个结构组成,我认为强制转换到第一个成员应该能够工作,但我不确定这是否正确,或者我是否在过程中忽略了填充细节。联合体是否需要填充上位位?

这似乎是未指明的行为?是否有人知道是否支持这种行为?我知道有一种替代标准的方法是使用一个带有枚举类型字段和struct container_storage成员的结构体,但考虑到这些信息已经在struct contained中,这似乎是浪费空间。

Linux下的编译命令:gcc -std=c99 -Wextra -pedantic -fstrict-aliasing test.c && ./a.out && echo $?返回0

#include <stdlib.h>
#include <assert.h>

enum type {type_a = 1, type_b = 2};

struct contained {
    int some_other_field;
    enum type type;
};

struct container_a {
    struct contained contained;
    int test;
};


struct container_b {
    struct contained contained;
    char test;
};

union container_storage {
    struct container_a container_a;
    struct container_b container_b;
};

int
main(int argc, char **argv)
{
    union container_storage a =
        {.container_a = {.contained = {.type = type_a}, .test = 42}};
    union container_storage b =
        {.container_b = {.contained = {.type = type_b}, .test = 'b'}};

    assert(((struct contained *)&a)->type == type_a);
    assert(((struct contained *)&b)->type == type_b);

    return EXIT_SUCCESS;
}

参考资料:

[1] gcc、strict-aliasing 和通过 union 进行强制类型转换

[2] 什么是 strict-aliasing 规则?

3个回答

5
这应该没问题。C11, 6.5.2.3/6 ("结构体和联合体的成员") 表示:
一个特殊的保证被提供以简化联合的使用:如果一个联合包含多个结构体并且这些结构体共享一个公共的初始序列(详见下文),并且如果联合对象当前包含其中一种结构,则允许在任何声明完整类型的联合可见的位置检查它们中任意一个的公共初始部分。 如果对应成员具有兼容类型(对于位字段,也具有相同的宽度)的一系列初始成员,则两个结构体共享一个公共初始序列
(C ++为标准布局联合提供了相同的保证(C ++11,9.2 / 18)。)

太好了听到这个消息。我一直在尝试查找C99规范,但似乎无法找到这个内容。谢谢! - backscattered
抱歉,结论是正确的(对于问题所询问的转换是明确定义的),但我会点踩,因为我认为这错误地指向了标准的错误部分。最相关的部分的措辞类似于:“适当转换后,指向结构体对象的指针指向其初始成员([关于位域的吹毛求疵]),反之亦然”,以及“适当转换后,指向联合对象的指针指向其每个成员([关于位域的吹毛求疵]),反之亦然”。 - mtraceur
1
这个问题询问了assert中的cast是否是良定义的,如果将其转换为struct contained *是良定义的,则->type就是良定义的,但如果转换不是良定义的,则共同初始序列规则无法帮助。共同初始序列规则使得ab上的->container_a.contained.type->container_b.contained.type都是有效访问(但值得注意的是,这个保证并不能防止联合体具有前导填充 - 关于转换它们的指针的单独保证才能做到这一点)。 - mtraceur

2

union 不会填充内存,它们只是将其成员重叠在一起。任何一个 struct 的第一个成员都保证直接开始,没有填充。通常情况下,以相同类型的相同成员开头的 struct 保证具有相同的布局。


1
根据C89标准,结构体类型的指针可以用于检查与存储数据类型共享公共初始序列的任何成员,包括联合体中的成员。这通常意味着任何结构体类型的指针都可以用于检查与任何其他类型共享的公共初始序列的任何成员(如果对象恰好是已声明联合体对象的成员,则该行为将被明确定义;在这些情况下,编译器产生所需行为的唯一实际方法是对所有行为进行维护)。
C99标准增加了一个额外的要求,即当完整的包含两个结构体的联合类型可见时,CIS保证仅适用于直接通过联合类型访问执行的访问,这使得某些编译器作者认为它仅适用于直接通过联合类型执行的访问。这些编译器的作者似乎认为需要处理具有公共头的函数的函数可能会出现以下情况:
struct smallThing { void *next; uint16_t length; uint8_t dat[2]; };
struct bigThing { void *next; uint16_t length; uint8_t dat[65528]; };

应该提取出类似于标题的标头,如下所示:


struct uHeader {  void *next; uint16_t length; };
struct smallThing { uHeader head; uint8_t dat[2]; };
struct bigThing { uHeader head; uint8_t dat[15994]; };

或者把所有东西都用联合类型对象表示,即使使用uHeader会增加struct smallThing的大小50%(并完全破坏任何依赖其布局的代码),并且对于大多数对象仅需要小型时,将所有东西都用联合体表示将增加内存使用量千倍。

如果需要使代码与基本忽略公共初始序列规则的编译器兼容,则应将公共初始序列规则视为基本无用。个人认为最好记录只有遵守CIS的编译器才应被视为适用于自己的代码,而不是为了迎合不适合的编译器而屈服,但我认为重要的是要意识到像后者这样的编译器存在。

据我所知,除非设置了-fno-strict-aliasing标志,否则clang和gcc没有以任何有用的方式遵守CIS规则。我不知道其他编译器。


这是一个很好的答案。我认为在其中编制一个不符合规范的编译器列表是个好主意。 - backscattered
1
@backscattered:我不知道有多少编译器像gcc和clang一样决定,强制遵守Common Initial Sequence保证的唯一方法是禁用所有基于类型的优化,但在我看来,这个保证的目的很明确,坚持它的成本应该远远低于对所有指针访问进行悲观假设的成本。 - supercat
@supercat,你有没有检查过通过联合指针进行转换并返回是否会强制GCC和Clang认识到可能发生的别名问题?首先,我会尝试在定义结构体的每个翻译单元中添加一个union smolBigg { struct smallThing smol; struct bigThing bigg; },然后看看这是否足以让它们认识到可能发生的别名问题。但如果这种方法失败了,也许将结构体指针转换为该联合体,然后再转换回来可能会起作用。((struct smallThing *)(union smolBigg * )foo)->next,在任何转换为void *之前也可以转换为联合体。 - mtraceur
@supercat 对不起,我们在说同一件事吗?联合体中的两个不同数组都不属于我所考虑的相关规则之一(除非指针转换允许适用于index为文字零的情况),因为此时它不是结构体的联合体,而是一组结构体数组的联合体,标准没有提到这一点(甚至在我们知道两个数组具有相同大小的项的特殊情况下也没有)。 - mtraceur
@supercat,然而我怀疑如果您定义了 union great_success { typeof(someUnion.oneArrayOfStructs[0]) a; typeof(someUnion.otherArrayofStructs[0]) b; }(或者用实际的类型文本替换不可移植的 typeof),那么以下四个中的任何一个都应该被 GCC 和 Clang 识别为合法的别名:((union great_success *)someUnion.oneArrayOfStructs)->a.cisMember((union great_success *)someUnion.oneArrayOfStructs)->b.cisMember((union great_success *)someUnion.otherArrayOfStructs)->a.cisMember((union great_success *)someUnion.otherArrayOfStructs)->b.cisMember - mtraceur
显示剩余7条评论

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