正如先前建立的,形式的联合
union some_union { type_a member_a; type_b member_b; ... };
与Ñ成员包括Ñ在重叠存储+ 1对象:联合本身一个目的并且对于每个联盟成员一个对象.很明显,您可以按任何顺序自由地读取和写入任何工会成员,即使读取的工会成员不是最后写入的工会成员.永远不会违反严格别名规则,因为访问存储的左值具有正确的有效类型.
脚注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.i
并且fi.f
指的是重叠的存储区域.阅读和写作是无条件的合法(一旦工会初始化,写作只是合法的).在我看来,所有主要编译器执行的先前讨论的优化产生了错误的代码,因为不同类型的两个指针合法地指向相同的位置.
我莫名其妙地无法相信我对严格别名规则的解释是正确的.由于前面提到的拐角情况,严格混叠的优化设计是不可能的,这似乎是不合理的.
请告诉我为什么我错了.
研究期间出现了一个相关问题.
请在添加自己的答案之前阅读所有现有答案及其评论,以确保您的答案添加了新的参数.
从您的示例开始:
int strict_aliasing_example(int *i, float *f) { *i = 1; *f = 1.0; return (*i); }
让我们首先承认,在没有任何联合的情况下,如果i
并且f
两者都指向同一个对象,这将违反严格的别名规则; 假定对象已经没有有效的类型,那么*i = 1
设置有效类型int
和*f = 1.0
然后将其设置为float
,与最终return (*i)
然后访问与有效类型的对象float
经由类型的左值int
,这显然是不允许的.
现在的问题是关于这是否仍达严格走样违反如果双方i
并f
指向同一个工会的成员.通过"."访问联盟成员.成员访问运算符,规范说(6.5.2.3):
后缀表达式后跟.运算符和标识符指定结构或联合对象的成员.该值是指定成员(95)的值,如果第一个表达式是左值,则该值是左值.
上面提到的脚注95说:
如果用于读取union对象的内容的成员与上次用于在对象中存储值的成员不同,则将值的对象表示的适当部分重新解释为新类型中的对象表示形式在6.2.6中描述(一个过程有时被称为''punning'').这可能是陷阱表示.
这显然是为了允许通过联合进行类型惩罚,但应该注意的是(1)脚注是非规范性的,也就是说,它们不应该禁止行为,而是应该澄清某些部分的意图.根据规范的其余部分的文本,以及(2)编译器供应商认为通过联合进行类型惩罚的这种限制仅适用于通过联盟成员访问操作员的访问 - 因为否则严格的别名是毫无意义的,因为任何潜在的别名访问也可能是同一联盟的潜在成员.
通过一个指向不存在的或至少非活性联合成员,由此您的示例存储要么提交一个严格别名冲突(因为它访问该成员是活性使用不适合类型的左值),或者使用其确实左值不表示一个对象(因为对应于非活动成员的对象不存在) - 它可以被论证,并且标准不是特别清楚,但是任何一种解释都意味着你的例子有未定义的行为.
(我可能会补充一点,我看不出脚注允许通过联合进行类型惩罚的脚本如何描述规范中固有的行为 - 也就是说,它似乎打破了不禁止行为的ISO规则;规范中似乎没有任何其他内容通过联合对任何形式的惩罚进行任何限制.此外,阅读规范性文本要求这种形式的惩罚要求必须通过联合类型立即进行访问,这是一种延伸.
但是,规范的另一部分经常会引起混淆,但在6.5.2.3中也是如此:
为了简化联合的使用,我们做了一个特殊的保证:如果一个联合包含几个共享一个共同初始序列的结构(见下文),并且如果联合对象当前包含这些结构中的一个,则允许检查公共其中任何一个的初始部分都可以看到完整类型的联合声明.
虽然这不适用于您的示例,因为没有共同的初始序列,我看到人们将此视为管理类型惩罚的一般规则(至少在涉及共同的初始序列时); 他们认为,这意味着只要完整的联合声明可见,就应该可以使用两个指向不同联盟成员的类型惩罚(因为在上面引用的段落中出现了这种效果的词).但是,我要指出,上面的段落仍然只适用于通过"."的工会成员访问.运营商.在这种情况下,协调这种理解的问题是,完整的联合声明必须是可见的,否则你将无法引用工会成员.我认为这是措辞中的这个小故障,加上示例3中类似的错误措辞(以下不是有效的片段(因为联合类型不可见......),当联合可见性不是真正的决定因素时) ,这使得一些人认为共同初始序列异常旨在全局应用,而不仅仅是通过"."进行成员访问.运算符,作为严格别名规则的例外; 并且,在得出这个结论之后,读者可能会将关于类型惩罚的脚注解释为全局应用,并且有些人会这样做:例如,参见关于此GCC错误的讨论(注意该错误已经处于暂停状态很长一段时间了) ).
(顺便说一句,我知道有几个编译器没有实现"全局公共初始序列"规则.我没有特别注意任何实现"全局公共初始序列"规则的编译器,同时也不允许任意类型的惩罚,但是并不意味着这样的编译器不存在.委员会对缺陷报告257的回应表明他们希望规则是全局的,但是,我个人认为仅仅是一种类型的可见性应该改变代码的语义.不是指那种类型存在严重缺陷,我知道其他人也同意.
此时,您可以很好地质疑如何通过成员访问运算符读取非活动的联合成员不会违反严格的别名,如果通过指针执行相同操作.这又是一个规范有些朦胧的领域; 关键在于决定哪个左值负责访问.例如,如果一个union对象u
有一个成员a
并且我通过表达式读取它u.a
,那么我们可以将其解释为成员对象的访问(a
)或者仅仅是对象u
的成员值的访问()摘自.在后一种情况下,没有别名冲突,因为它特别允许通过包含合适成员(6.57)的聚合类型的左值来访问对象(即活动成员对象).实际上,6.5.2.3中成员访问运算符的定义确实支持这种解释,如果有点弱:值是指定成员的值 - 虽然它可能是左值,但是没有必要访问由此引用的对象. lvalue是为了获取成员的值,因此避免了严格的别名冲突.但这再次拉伸了一点.
(对我而言,似乎是指定不足,通常,只是当一个对象按照6.5左右的"左右表达式访问它的存储值"时;我们当然可以为自己做出合理的决定,但是我们必须如上所述,小心允许通过工会进行打字,或者不愿意忽视脚注95.尽管经常有不必要的措辞,但规范有时缺乏必要的细节.
关于联合语义的争论总是在某些时候引用DR 236.实际上,您的示例代码表面上与该缺陷报告中的代码非常相似.我会注意到:
"委员会认为示例2违反了6.5第7段中的别名规则" - 这与我上面的推理并不矛盾;
"为了不违反规则,示例中的函数f应写为" - 这支持了我上面的推理; 您必须使用union对象(和"."运算符)来更改活动成员类型,否则您将访问不存在的成员(因为union一次只能包含一个成员);
DR 236中的示例与类型惩罚无关.它是否可以通过指向该成员的指针分配给非活动的联合成员.有问题的代码与此处的问题略有不同,因为它在写入第二个成员后不会再次尝试访问"原始"联合成员.因此,尽管示例代码中存在结构相似性,但缺陷报告与您的问题基本无关.
委员会在DR 236中的答复声称"两个程序都引用了未定义的行为".然而,讨论不支持这一点,该讨论仅显示示例2调用未定义的行为.我认为反应是错误的.
根据§6.5.2.3中工会成员的定义:
3后缀表达式后跟
.
运算符,标识符指定结构或联合对象的成员....4后缀表达式后跟
->
运算符,标识符指定结构或联合对象的成员....
另见§6.2.31:
结构或工会的成员; 每个结构或联合为其成员都有一个单独的名称空间(通过用于通过
.
或->
运算符访问成员的表达式的类型消除歧义);
很明显,脚注95指的是工会成员在范围内使用联盟并使用.
或->
运营商.
由于对包含联合的字节的赋值和访问不是通过联合成员而是通过指针进行的,因此您的程序不会调用联合成员的别名规则(包括脚注95所阐明的那些).
此外,由于后面的对象的有效类型*f = 1.0
是float
,但是违反了正常的别名规则,但是它的存储值是由类型的左值访问的int
(参见§6.57).
注意:所有参考文献都引用了 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.116中的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; }