是否错误地指定了严格别名规则?

如前所述,forms的联合

union some_union { type_a member_a; type_b member_b; ... }; 

n个成员在重叠存储中包含n + 1个对象:一个对象用于union本身,一个对象用于每个union成员。 很明显,您可以按任何顺序自由地读取和写入任何工会成员,即使读取的工会成员不是最后写入的工会成员。 严格别名规则永远不会被违反,因为您访问存储的左值具有正确的有效类型。

脚注95 进一步支持了这一点,脚注95解释了类型双关语是否是联盟的预期用途。

严格别名规则启用的优化的典型示例是此函数:

 int strict_aliasing_example(int *i, float *f) { *i = 1; *f = 1.0; return (*i); } 

编译器可以优化到类似的东西

 int strict_aliasing_example(int *i, float *f) { *i = 1; *f = 1.0; return (1); } 

因为它可以安全地假设写入*f不会影响*i的值。

但是,当我们将两个指针传递给同一个联盟的成员时会发生什么? 考虑这个例子,假设一个典型的平台,其中float是IEEE 754单精度浮点数, int是32位二进制补码整数:

 int breaking_example(void) { union { int i; float f; } fi; return (strict_aliasing_example(&fi.i, &fi.f)); } 

如前所述, fi.ifi.f指的是重叠的存储区域。 阅读和编写它们是无条件合法的(一旦联盟被初始化,写作只是合法的)。 在我看来,由所有主要编译器执行的先前讨论的优化产生不正确的代码,因为不同类型的两个指针合法地指向相同的位置。

我莫名其妙地无法相信我对严格别名规则的解释是正确的。 由于前面提到的拐角情况,严格混叠的优化设计是不可能的,这似乎是不合理的。

请告诉我为什么我错了。

研究期间出现了一个相关问题 。

请在添加自己的答案之前阅读所有现有答案及其评论,以确保您的答案添加了新的参数。

从您的示例开始:

 int strict_aliasing_example(int *i, float *f) { *i = 1; *f = 1.0; return (*i); } 

让我们首先承认,如果没有任何联合,如果if都指向同一个对象,这将违反严格的别名规则; 假设对象没有有效类型,则*i = 1将有效类型设置为int并且*f = 1.0然后将其设置为float ,然后最终return (*i)通过左值访问具有有效类型float的对象int类型,显然是不允许的。

问题是如果if指向同一个联盟的成员,这是否仍然构成严格别名违规。 通过“。”访问联盟成员。 成员访问运算符,规范说(6.5.2.3):

后缀表达式后跟。 运算符和标识符指定结构或联合对象的成员。 该值是指定成员(95)的值,如果第一个表达式是左值,则该值是左值。

上面提到的脚注95说:

如果用于读取union对象内容的成员与上次用于在对象中存储值的成员不同,则将值的对象表示的相应部分重新解释为新类型中的对象表示forms6.2.6中描述的过程(有时称为”punning”)。 这可能是陷阱表示。

这显然是为了通过联盟允许类型惩罚,但应该注意的是(1)脚注是非规范性的,也就是说,它们不应该禁止行为,而是应该澄清某些部分的意图。根据规范的其余部分提供的文本,以及(2)编译器供应商认为通过联合进行类型惩罚的这种限制仅适用于通过联盟成员访问操作员进行访问 – 因为否则严格的别名是毫无意义的,因为任何潜在的别名访问也可能是同一联盟的潜在成员。

您的示例通过指向不存在或至少非活动的联合成员的指针存储,从而提交严格的别名冲突(因为它使用不合适类型的左值访问活动的成员)或使用左值不表示一个对象(因为对应于非活动成员的对象不存在) – 它可以被论证,并且标准不是特别清楚,但是任何一种解释都意味着你的例子有未定义的行为。

(我可能会补充一点,我看不出脚注如何通过联合允许类型惩罚描述规范中固有的行为 – 也就是说,它似乎打破了不禁止行为的ISO规则;规范中似乎没有其他内容通过联合来进行类型惩罚的任何限制。此外,阅读规范性文本要求这种forms的惩罚要求必须通过联合类型立即进行访问,这是一种延伸。

但是,规范的另一部分经常会引起混淆,但在6.5.2.3中也是如此:

为了简化联合的使用,我们做了一个特殊的保证:如果一个联合包含几个共享一个共同初始序列的结构(见下文),并且如果联合对象当前包含这些结构中的一个,则允许检查公共其中任何一个的初始部分都可以看到完整类型的联合声明。

虽然这不适用于您的示例,因为没有共同的初始序列,我看到人们将此视为管理类型惩罚的一般规则(至少在涉及共同的初始序列时); 他们认为这意味着只要完整的联合声明可见 ,就应该可以使用两个指向不同联盟成员的类型惩罚(因为这个效果的词出现在上面引用的段落中)。 但是,我要指出上面的段落仍然只适用于通过“。”的工会成员访问。 运营商。 在这种情况下,协调这种理解的问题是,完整的联合声明必须是可见的,否则你将无法引用工会成员。 我认为这是措辞中的这个小故障,加上示例3中类似的错误措辞( 以下不是有效的片段(因为联合类型不可见……) ,当联合可见性不是真正的决定因素时) ,这使得一些人认为共同初始序列exception旨在全局应用,而不仅仅是通过“。”进行成员访问。 运算符,作为严格别名规则的例外; 并且,在得出这个结论之后,读者可能会将关于类型惩罚的脚注解释为全局应用,并且有些人会这样做:例如,参见关于此GCC错误的讨论(注意该错误已经处于暂停状态很长一段时间了) )。

(顺便说一句,我知道有几个编译器没有实现“全局公共初始序列”规则。我没有特别注意任何实现“全局公共初始序列”规则的编译器,同时也不允许任意类型的惩罚,但是并不意味着这样的编译器不存在。委员会对缺陷报告257的回应表明他们希望规则是全局的,但是,我个人认为仅仅是一种类型的可见性应该改变代码的语义。不是指那种类型存在严重缺陷,我知道其他人也同意。

此时,您可以很好地质疑如何通过成员访问运算符读取非活动联合成员不违反严格别名,如果通过指针执行相同操作。 这又是规范有些朦胧的领域; 关键在于决定哪个左值负责访问。 例如,如果一个联合对象u有一个成员a并且我通过表达式ua读取它,那么我们可以将其解释为成员对象的访问( a )或仅仅是对联合对象( u )的访问。然后从中提取成员值。 在后一种情况下,没有别名冲突,因为它特别允许通过包含合适成员(6.5¶7)的聚合类型的左值来访问对象(即活动成员对象)。 实际上,6.5.2.3中成员访问运算符的定义确实支持这种解释,如果有点弱: 值是指定成员的值 – 虽然它可能是左值,但是没有必要访问由此引用的对象。 lvalue是为了获取成员的值,因此避免了严格的别名冲突。 但这再次拉伸了一点。

(对我来说,似乎是指定不足,通常,当一个对象按照6.5左右的方式“通过左值表达式访问它的存储值”时;我们当然可以为自己做出合理的决定,但是我们必须如上所述,小心允许通过工会进行打字,或者不愿意忽视脚注95.尽管经常有不必要的措辞,但规范有时缺乏必要的细节。

关于联合语义的争论总是在某些时候引用DR 236 。 实际上,您的示例代码表面上与该缺陷报告中的代码非常相似。 我会注意到:

  1. “委员会认为示例2违反了6.5第7段中的别名规则” – 这与我上面的推理并不矛盾;
  2. “为了不违反规则,示例中的函数f应写为” – 这支持了我上面的推理; 您必须使用union对象(和“。”运算符)来更改活动成员类型,否则您正在访问一个不存在的成员(因为union一次只能包含一个成员);
  3. DR 236中的示例与类型惩罚无关 。 它是关于是否可以通过指向该成员的指针分配给非活动的联合成员。 有问题的代码与此处的问题略有不同,因为它在写入第二个成员后不会再次尝试访问“原始”联合成员。 因此,尽管示例代码中存在结构相似性,但缺陷报告与您的问题基本无关。
  4. 委员会在DR 236中的答复声称“两个程序都引用了未定义的行为”。 但是,讨论不支持这一点,该讨论仅显示示例2调用未定义的行为。 我认为反应是错误的。

根据§6.5.2.3中工会成员的定义:

3后缀表达式后跟. 运算符和标识符指定结构或联合对象的成员。 …

4后缀表达式后跟->运算符,标识符指定结构或联合对象的成员。 …

另见§6.2.3¶1:

  • 结构或工会的成员; 每个结构或联合为其成员都有一个单独的名称空间(通过.->运算符用于访问成员的表达式的类型消除歧义);

很明显,脚注95指的是工会成员在范围内使用工会并使用工会成员.->运算符。

由于对包含联合的字节的赋值和访问不是通过联合成员而是通过指针进行的,因此您的程序不会调用联合成员的别名规则(包括脚注95所阐明的那些)。

此外,由于*f = 1.0之后的对象的有效类型是float ,因此违反了正常的别名规则,但其存储的值由int类型的左值访问(参见§6.5¶7)。

注意:所有参考文献都引用了 C11标准草案。

C11标准(§6.5.2.3.9例3)有以下例子:

以下不是有效的片段(因为联合类型在函数f中不可见):

  struct t1 { int m; }; struct t2 { int m; }; int f(struct t1 *p1, struct t2 *p2) { if (p1->m < 0) p2->m = -p2->m; return p1->m; } int g() { union { struct t1 s1; struct t2 s2; } u; /* ... */ return f(&u.s1, &u.s2); } 

但我无法对此发现更多澄清。

严格别名规则禁止通过两个没有兼容类型的指针访问同一个对象,除非一个是指向字符类型的指针:

7对象的存储值只能由具有以下类型之一的左值表达式访问:88)

  • 与对象的有效类型兼容的类型,
  • 与对象的有效类型兼容的类型的限定版本,
  • 与对象的有效类型对应的有符号或无符号类型的类型,
  • 与有效类型的对象的限定版本对应的有符号或无符号类型的类型,
  • 在其成员中包含上述类型之一的聚合或联合类型(包括递归地,子聚合或包含联合的成员),或者
  • 一个字符类型。

在您的示例中, *f = 1.0; 正在修改fi.i ,但类型不兼容。

我认为错误在于认为联合包含n个对象,其中n是成员的数量。 在程序执行期间,§6.7.2.1¶16中的union在任何时候只包含一个活动对象

最多一个成员的值可以随时存储在union对象中。

支持这种解释,即联合不同时包含其所有成员对象,可以在§6.5.2.3中找到:

如果union对象当前包含其中一个结构

最后,在2006年的缺陷报告236中提出了一个几乎相同的问题。

例2

 // optimization opportunities if "qi" does not alias "qd" void f(int *qi, double *qd) { int i = *qi + 2; *qd = 3.1; // hoist this assignment to top of function??? *qd *= i; return; } main() { union tag { int mi; double md; } u; u.mi = 7; f(&u.mi, &u.md); } 

委员会认为,示例2违反了6.5第7段中的别名规则:

“在其成员中包含上述类型之一的聚合或联合类型(包括递归地,子聚合或包含联盟的成员)。”

为了不违反规则,示例中的函数f应写为:

 union tag { int mi; double md; } u; void f(int *qi, double *qd) { int i = *qi + 2; u.md = 3.1; // union type must be used when changing effective type *qd *= i; return; } 

本质上,严格别名规则描述了允许编译器假定(或者,相反地,不允许假设)两个不同类型的指针不指向存储器中的相同位置的情况。

在此基础上,允许在strict_aliasing_example()描述的优化,因为允许编译器假定fi指向不同的地址。

breaking_example()使传递给strict_aliasing_example()的两个指针指向同一个地址。 这打破了允许strict_aliasing_example()的假设,因此导致该函数表现出不确定的行为。

因此,您描述的编译器行为是有效的。 事实上, breaking_example()导致传递给strict_aliasing_example()的指针指向导致未定义行为的相同地址 – 换句话说, breaking_example()打破了允许编译器在strict_aliasing_example()内进行的strict_aliasing_example()

让我们暂时退出标准,并考虑编译器实际可行的内容。

假设strict_aliasing_example()strict_aliasing_example.c定义,并且breaking_example()breaking_example.c定义。 假设这两个文件分别编译然后链接在一起,如下所示:

 gcc -c -o strict_aliasing_example.o strict_aliasing_example.c gcc -c -o breaking_example.o breaking_example.c gcc -o breaking_example strict_aliasing_example.o breaking_example.o 

当然我们必须在breaking_example.c添加一个函数原型,如下所示:

int strict_aliasing_example(int *i, float *f);

现在考虑gcc的前两次调用是完全独立的,除了函数原型之外不能共享信息。 当编译器生成strict_aliasing_example()代码时,编译器不可能知道ij将指向同一个union的成员。 在链接或类型系统中没有任何东西可以指定这些指针在某种程度上是特殊的,因为它们来自一个联合。

这支持了其他答案提到的结论:从标准的角度来看,通过访问联合.->与解除引用任意指针相比,遵循不同的别名规则。

在C89标准之前,绝大多数实现将写入解除引用的行为定义为特定类型的指针,即以为该类型定义的方式设置底层存储的位,并定义读取 – 解引用指针的行为特定类型的,以为该类型定义的方式读取底层存储的位。 虽然这些能力对所有实现都没有用,但是有许多实现可以通过例如使用32位加载和存储来一次操作四个字节的组来大大提高热循环的性能。 此外,在许多这样的实现中,支持这样的行为并没有花费任何成本。

C89标准的作者声明他们的目标之一是避免无可挽回地破坏现有代码,并且有两种基本方式可以解释规则与此一致:

  1. C89规则可能只适用于类似于基本原理中给出的情况(通过该类型直接访问具有声明类型的对象并通过指针间接访问),以及编译器没有理由期望别名的情况 。 跟踪每个变量是否当前缓存在寄存器中非常简单,并且能够在访问其他类型的指针时将这些变量保存在寄存器中是一种简单而有用的优化,并且不会妨碍对使用更常见的代码的支持别名模式(让编译器将float*解释为int* cast,因为需要刷新任何寄存器缓存的float值是简单而直接的;这种转换非常罕见,以至于这种方法不太可能对性能产生负面影响)。

  2. 鉴于标准对于为给定平台提供高质量实现的内容通常是不可知的,规则可以被解释为允许实现破坏使用别名的代码,这些代码既有用又明显,没有表明好的质量实施不应该试图避免这样做。

如果标准定义了一种允许就地混叠的实用方法,这种方法在任何方面都不会明显低于其他方法,那么除了定义的方法之外的方法可能被合理地视为已弃用。 如果不存在标准定义的方法,那么为了获得良好性能而需要别名的平台的质量实现应该努力有效地支持这些平台上的公共别名平台,无论标准是否要求它们这样做。

遗憾的是,由于标准要求的内容不明确,导致某些人认为不存在替代品的弃用结构。 存在涉及两个基本类型的完整联合类型定义被解释为指示通过一种类型的指针的任何访问应该被视为对另一种类型的可能访问将使得可以调整依赖于就地别名的程序。这样做没有未定义的行为 – 鉴于本标准,这是任何其他实际方法无法实现的。 不幸的是,这样的解释也会限制99%无害的情况下的许多优化,从而使解释标准的编译器无法以尽可能高效的方式运行现有代码。

至于规则是否正确指定,这将取决于它应该是什么意思。 可能有多种合理的解释,但将它们结合起来会产生一些相当不合理的结果。

PS–the only interpretation of the rules regarding pointer-comparisons and memcpy that would make sense without giving the term “object” a meaning different from its meaning in the aliasing rules would suggest that no allocated region can be used to hold more than a single kind of object. While some kinds of code might be able to abide such a restriction, it would make it impossible for programs to use their own memory management logic to recycle storage without excessive numbers of malloc/free calls. The authors of the Standard may have intended to say that implementations are not required to let programmers create a large region and partition it into smaller mixed-type chunks themselves, but that doesn’t mean that they intended general-purpose implementations would fail to do so.

Here is note 95 and its context:

A postfix expression followed by the . operator and an identifier designates a member of a structure or union object. The value is that of the named member, (95) and is an lvalue if the first expression is an lvalue. If the first expression has qualified type, the result has the so-qualified version of the type of the designated member.

(95) If the member used to read the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called “type punning”). 这可能是陷阱表示。

Note 95 clearly applies to an access via a union member. Your code does not do that. Two overlapping objects are accessed via pointers to 2 separate types, none of which is a character type, and none of which is a postfix expression pertinent for type punning.

This is not a definitive answer…

The Standard does not allow the stored value of a struct or union to be accessed using an lvalue of the member type. Since your example accesses the stored value of a union using lvalues whose type is not that of the union, nor any type that contains that union, behavior would be Undefined on that basis alone.

The one thing that gets tricky is that under a strict reading of the Standard, even something so straightforward as

 int main(void) { struct { int x; } foo; foo.x = 1; return 0; } 

also violates N1570 6.5p7 because foo.x is an lvalue of type int , it is used to access the stored value of an object of type struct foo , and type int does not satisfy any of the conditions on that section.

The only way the Standard can be even remotely useful is if one recognizes that there need to be exceptions to N1570 6.5p7 in cases involving lvalues that are derived from other lvalues. If the Standard were to describe cases where compilers may or must recognize such derivation, and specify that N1570 6.5p7 only applies in cases where storage is accessed using more than one type within a particular execution of a function or loop, that would have eliminated a lot of complexity including any need for the notion of “Effective Type”.

Unfortunately, some compilers have taken it upon themselves to ignore derivation of lvalues and pointers even in some obvious cases like:

 s1 *p1 = &unionArr[i].v1; p1->x ++; 

It may be reasonable for a compiler to fail to recognize the association between p1 and unionArr[i].v1 if other actions involving unionArr[i] separated the creation and use of p1, but neither gcc nor clang can consistently recognize such association even in simple cases where the use of the pointer immediately follows the action which takes the address of the union member.

Again, since the Standard doesn’t require that compilers recognize any usage of derived lvalues unless they are of character types, the behavior of gcc and clang does not make them non-conforming. On the other hand, the only reason they are conforming is because of a defect in the Standard which is so outrageous that nobody reads the Standard as saying what it actually does.