为什么尽管 C 有严格的别名规则,但仍允许引用具有相似第一成员的结构?

Why is the reference of structs with similar first members allowed despite C strict aliasing rules?

首先,如果这看起来是重复的,我深表歉意,但我在其他地方找不到这个问题

我正在通读 N1570,特别是 §6.5¶7,内容为:

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:
— a type compatible with the effective type of the object,
— a qualified version of a type compatible with the effective type of the object,
— a type that is the signed or unsigned type corresponding to the effective type of the object,
— a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
— an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
— a character type.

这让我想起了我在(类似 BSD 的)套接字编程中看到的一个常见习语,尤其是在 connect() 调用中。虽然 connect() 的第二个参数是 struct sockaddr *,但我经常看到传递给它的 struct sockaddr_in *,这似乎有效,因为它们共享一个相似的初始元素。我的问题是:

这种情况适用于上述规则中详述的哪些意外事件,为什么,或者现在未定义的行为是以前标准的产物?

此行为未由 C 标准定义。

此行为由 The Single Unix Specification and/or 与您正在使用的软件相关的其他文档定义,尽管部分是隐含的。

“对象 的存储值只能由……访问”的措辞具有误导性。 C 标准不能强迫你做任何事情;您没有义务遵守其“应”要求。就 C 标准而言,不遵守其要求的唯一后果是 C 标准未定义行为。这并不禁止其他文档定义行为。

the netinet/in.h documentation中,我们看到“sockaddr_in结构用于存储Internet协议族的地址。这种类型的值必须转换为 struct sockaddr 才能与本文档中定义的套接字接口一起使用。”所以文档不仅告诉我们应该,而且必须将 sockaddr_in 转换为 sockaddr。我们必须这样做的事实意味着该软件支持它并且它将起作用。 (请注意,这里的措辞不准确;我们实际上并没有将 sockaddr_in 转换为 sockaddr,而是实际转换了指针,导致内存中的 sockaddr_in 对象被视为 sockaddr.)

因此,为 Unix 实现提供的操作系统、库和开发人员工具都支持这一点。

这是对 C 语言的扩展:在 C 标准未定义行为的地方,其他文档可能会提供定义,并允许您编写无法单独使用 C 标准编写的软件。 C 标准所说的 undefined 的行为并不是被禁止的行为,而是一个空的 space 可以被其他规范填充。

关于公共初始序列的规则可以追溯到 1974 年。关于“严格别名”的最早规则只能追溯到 1989 年。后者的目的并不是要它们胜过其他一切,而仅仅是允许编译器执行优化 他们的客户会觉得有用 而不会被标记为不合格。该标准明确指出,在标准的一部分 and/or 实现的文档将描述某些操作的行为但标准的另一部分将其描述为未定义行为的情况下,实现可能会选择优先考虑第一个,并且基本原理清楚地表明,作者认为“市场”比委员会更适合确定实施时间。

在充分迂腐地阅读 N1570 6.5p7 约束的情况下,几乎所有程序都违反了这些约束,但除非实现足够迟钝,否则违反方式无关紧要。该标准没有尝试列出一种类型的对象可以被另一种左值访问的所有情况,而是列出编译器必须允许一种类型的对象可以被 访问的所有情况。看似无关的 另一个左值。给定代码序列:

int x;
int *p[10];
p[2] = &someStruct.intMember;
...
*p[2] = 23;
x = someStruct.intMember;

在 6.5p7 中没有规则的情况下,除非编译器跟踪 p[2] 的来源,否则它没有理由识别 someStruct.member 的读取可能以存储为目标那只是用 *p[2] 写的。另一方面,给定代码:

int x;
int *p[10];
...
someStruct.intMember = 12;
p[2] = &someStruct.intMember;
x = *p[2];

在这里,没有规则实际上允许与结构关联的存储被该成员类型的左值访问,但除非编译器故意盲目,否则它能够在第一次分配给 someStruct.intMember,该成员的地址正在被占用,应该:

  1. 说明将用结果指针完成的所有操作,如果它能够这样做的话,或者
  2. 不要假设结构的存储不会在使用结构类型的先前和后续操作之间访问。

我认为编写后来重新编号为 N1570 6.5p7 的规则的人从来没有想过,这些规则会被解释为禁止利用通用初始序列规则的通用模式。如前所述,大多数程序都违反了 6.5p7 的约束,但这样做的方式可以被任何不迟钝的编译器预测地处理;那些使用公共初始序列保证的人将属于这一类。由于该标准的作者认识到“符合”编译器的可能性,该编译器只能有意义地处理一个人为且无用的程序,因此迟钝的编译器可能滥用“别名规则”这一事实并不被视为缺陷。