gcc和clang之间的一个细节差异
2014-08-19
事情要从Wikipedia上的一段代码说起:
1 #include <cstdlib>
2 #include <iostream>
3
4 struct A {};
5 struct E {};
6
7 class T {
8 public:
9 T() {
10 throw E();
11 }
12 };
13
14 void *operator new(std::size_t, const A &) {
15 std::cout << "Placement new called." << std::endl;
16 }
17
18 void operator delete(void *, const A &) {
19 std::cout << "Placement delete called." << std::endl;
20 }
21
22 int main() {
23 A a;
24 try {
25 T * p = new (a) T;
26 } catch (E exp) {
27 std::cout << "Exception caught." << std::endl;
28 }
29 return 0;
30 }
编译后运行,提示:illegal hardware instruction (core dumped)
。
当时我想,这不科学啊,Wikipedia毕竟不同于那些《21天学会XXX》,如果包含错误的代码,很大概率地会被其他读者看到并且修正,一定是我打开方式不对。而且诡异的是,出错提示上竟然写着instruction,这在使用编译器生成的程序中是十分罕见的。
检查了编译这段代码用的命令,用了clang++,于是本着撞大运编程的精神,换成g++。运行结果正确。
另外,clang++对重载的new操作符没有返回值给了一个警告,而g++没有。
考虑到代码不长,直接删去输出之类的部分,很快定位到了问题所在,正是那个没有返回值的new操作符。
1 void *operator new(unsigned long) {} // here!
2
3 int main() {
4 new int;
5 return 0;
6 }
对于这个new,g++产生了一个空的函数:
1 _Znwm:
2 pushq %rbp
3 movq %rsp, %rbp
4 movq %rdi, -8(%rbp)
5 popq %rbp
6 ret
而clang++用一条错误指令ud2
(undefined),强行让程序抛出错误:
1 _Znwm:
2 pushq %rbp
3 movq %rsp, %rbp
4 movq %rdi, -16(%rbp)
5 ud2
为了进一步探究,我们对上面那个程序稍作改动:
1 void *xxx() {}
2
3 int main() {
4 int *a = (int *) xxx();
5 *a = 1;
6
7 return 0;
8 }
g++仍然一声不吭地编译通过,程序在对*a
赋值时报出segmentation fault,而clang++仍然产生illegal instruction。
此时我忽然想到了,莫非clang对所有缺失return的代码都会产生错误?我们做得更彻底一些:
1 int mian() {}
2 int main() {mian();}
果然还是illegal instruction!我们可以发现,mian
函数包含ud2
指令,而main
函数并不包含。这可能是因为现存太多没有在main
中显式地返回状态码的程序。
带返回值的函数没有return,在标准中是未定义的行为。对于未定义行为,不同的编译器可以采取不同的措施,可以沉默处理,可以警告,可以产生编译时或运行时的错误,甚至可以运行一个游戏。
clang(clang++)的做法值得欣赏,更快地出错意味着对错误的定位会更加容易,许多新兴的语言、编译器都认可了这个观点。当然,gcc(g++)不这样做,或许有兼容C/C++早期代码的考虑。这从一个侧面体现了两款编译器在设计思路上的不同。
顺带一提,还有一个有意思的细节,clang的输出如果覆盖了原有的文件,那么它的可执行属性会被强制覆盖,但gcc对可执行属性采取的策略是只添加不去除。