你有任何恐怖故事要讲吗?GCC手册最近新增了一条关于-fstrict-aliasing和通过联合转换指针的警告:
[...] 取地址,将结果指针强制转换并解引用结果具有未定义的行为 [强调添加],即使转换使用联合类型,例如:
union a_union {
int i;
double d;
};
int f() {
double d = 3.0;
return ((union a_union *)&d)->i;
}
有没有例子可以说明这个未定义行为?
注意,这个问题不是关于C99标准的规定或不规定。而是关于现今gcc和其他编译器的实际运行情况。
我猜测,一个潜在的问题可能在于将d设置为3.0。因为d是一个临时变量,从来没有直接读取过,也没有通过“相当兼容”的指针读取过,所以编译器可能不会费心去设置它。然后f()将从堆栈中返回一些垃圾值。
我的简单、天真的尝试失败了。例如:
#include <stdio.h>
union a_union {
int i;
double d;
};
int f1(void) {
union a_union t;
t.d = 3333333.0;
return t.i; // gcc manual: 'type-punning is allowed, provided...' (C90 6.3.2.3)
}
int f2(void) {
double d = 3333333.0;
return ((union a_union *)&d)->i; // gcc manual: 'undefined behavior'
}
int main(void) {
printf("%d\n", f1());
printf("%d\n", f2());
return 0;
}
运行良好,在CYGWIN上:
-2147483648
-2147483648
看汇编代码,我们可以发现gcc已经完全优化掉了t
: f1()
只是简单地存储预先计算好的答案:
movl $-2147483648, %eax
当f2()
将3333333.0推入浮点数栈中,然后提取返回值:
flds LC0 # LC0: 1246458708 (= 3333333.0) (--> 80 bits)
fstpl -8(%ebp) # save in d (64 bits)
movl -8(%ebp), %eax # return value (32 bits)
这些函数也是内联的(这似乎是一些微妙的严格别名错误的原因),但这与本文无关。(这个汇编程序也不太相关,但它增加了证实的细节。)
另外要注意的是,取地址显然是错误的(或者如果您试图说明未定义行为,则是正确的)。例如,就像我们知道这是错误的:
extern void foo(int *, double *);
union a_union t;
t.d = 3.0;
foo(&t.i, &t.d); // undefined behavior
我们同样知道这是错误的:
extern void foo(int *, double *);
double d = 3.0;
foo(&((union a_union *)&d)->i, &d); // undefined behavior
关于这个问题的背景讨论,可以参考以下内容:
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1422.pdf
http://gcc.gnu.org/ml/gcc/2010-01/msg00013.html
http://davmac.wordpress.com/2010/02/26/c99-revisited/
http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html
(= 谷歌搜索页面,然后查看缓存页面)
什么是严格别名规则?
C++中的C99严格别名规则(GCC)
在第一个链接中,ISO会议的草案记录显示,在第4.16节中,一位与会者指出:
有没有人认为规则足够清晰明了? 没有人真正能够解释它们。
其他注意事项:我的测试使用gcc 4.3.4,并带有-O2; 选项-O2和-O3意味着-fstrict-aliasing。 GCC手册中的示例假定sizeof(double) >= sizeof(int); 如果它们不相等,则无关紧要。
此外,正如Mike Acton在cellperformace链接中指出的那样,-Wstrict-aliasing = 2
,但不是=3
,对于此处的示例会产生warning: dereferencing type-punned pointer might break strict-aliasing rules
。