编译器与标准——严格还是智能?

2009/03/21 | 14:21 | 分类:计算机科学与编程 | 标签: | 705次阅读

  最近做的C/C++工程需要在多种平台下编译(下面所说的编译泛指编译、连接等过程)并运行通过,多种不同版本的编译器考验着代码正确性和严格性。经测试,我们的历史代码中存在着这样几类问题,使得它们在VS2008和低版本gcc下编译通过,但在高版本的gcc下编译就会或多或少地报错:
  1、使用了外部的函数,但没有在本文件中声明或#include对应的头文件。VS2008和gcc3.2在很多情况下可以智能地从一同编译的其它文件或标准库中找到对应的函数并连接,而gcc4.3要求必须声明。
  2、使用模板时,T::name表示T::name类型还是T类的name成员?即使不使用typename关键字,VS2008和gcc3.2在很多情况下可以智能推断T::name a;中的T::name表示表示一个类型。但在gcc4.1/4.3下必须使用typename T::name来表示T::name类型。
  3、隐式类型转换。对于函数Func(string&),在VS2008中可以采用Func(string("new"));来调用,但gcc不能隐式地转换构造函数的返回,需要使用string str("new"); Func(str);来调用。
  4、一些copy-paste粗心小问题,比如声明类的成员函数时不应该加“类名::”限定。如果加了,VS2008会忽略,而gcc4.1/4.3会报错。
  5、非标准的库函数,比如VS2008的itoa(_itoa)。如果说上述几条是“严格性”的问题,这个应该算是“正确性”的问题了———个编译器扩展的内容在另一个编译器看来自然是不知所云。当然,为方便起见,使用编译器提供的非标准扩展也是可以的(比如在不完全支持C99的gcc和VC版本下,分别使用atoq和_atoi64来代替C99的atoll),但这时候别忘了使用条件编译,针对不同的编译器提供不同的代码。
  等等。
  总体来看,我们用到的几个编译器的严格程度如下。注意仅仅是从一些小的侧面反映的结果,不完全准确,说明大致情况而已。

  ─────── 更严格 ──────→
   VS2008  gcc3.2  gcc4.1  gcc4.3
  ←────── 更智能 ───────

  编译器的严格性也许是一把双刃剑。但我个人认为严格一些还是利大于弊的。拿上面的问题1来说,我就遇到过gcc3.2“智能”连接错误的问题,而如果使用gcc4.3,这个bug就可以轻易地被定位,节约大量的调试时间。
  智能的底线是正确性。如果代码中出现了不规范的写法,编译器给出智能判断并忽略问题的同时,应该给程序员一定的警告信息,让他们核实代码并判断是否需要向标准的方向修改。不过可悲的是编译器的警告往往被程序员们一略而过了。也许正是因为这一点,gcc才变得越发严格,试图和程序员划清责任域,减少扯皮现象?
  当然,仅仅写出语法上严格的代码,解决了不同编译器下编译通过的问题,我们还不能保证程序运行时语义的正确性。例如程序中存在未初始化的变量,一种编译器的实现是以0填充,恰好与程序员的原意相符;而切换到另一编译器下,未初始化的变量成了随机值,程序运行就会出问题。所以,对C/C++这样高度自由的语言来说,严格的编译器也不是万能的,人的因素始终是第一位的。
  说了这么些严格和智能,评判它们的依据是什么?自然是语言的标准了(C99、ISO C++之类)。有关编译器对语言(包含库)标准的支持,我们大约可以按以下维度划分:

      标准要求实现的
         ↑
         │
  标准未定义的 │ 标准定义的
     ←───┼───→
         │
         │
         ↓
      编译器扩展的

  标准定义的语法在任何编译器下都应该有相同、无歧义的语义。尽管其内部实现可能不同,或是依赖于具体的机器架构,但在标准中总有明确的说明。而标准中也存在大量未定义的语义,这通常是为了让编译器作者放开限制,做更高效、更简单的实现。这类语法在不同的编译器或不同的运行环境下可能产生不同的结果,经典的例子就是n=m+++m++这种副作用语句的叠加,再如L'abcd'表示什么的问题。
  工程中通常应该严格禁用标准未定义的语法,因为机器架构、编译器种类和版本、优化选项、运行时环境等的变化都有可能对程序的运行结果产生不可预知的影响。但有些时候标准未定义的的语法也是有用的,比如为了学习理解编译器的原理和优化算法。另外在一些技巧性游戏,如Quine,以及IOCCCTime Limit Exceeded这样的hack竞赛中,使用未定义语法的捷径也是未尝不可的。
  而编译器扩展的语法和库则是一类相对有用的工具。不同于标准未定义的内容,编译器所做的扩展也是有严格定义的,不会产生不可预知的结果。在对代码的迁移性要求不高的场合直接使用并无大碍。如果对代码的迁移性有要求,那就如上文所述使用条件编译来限制其作用范围。毕竟编译器所做的扩展是为了弥补标准库的不足,方便开发者。如果对代码将来的使用范围有明显的预知,就需要权衡是否使用编译器的扩展特性。不过就连Linux内核的代码都在使用gcc的扩展语法
  在C/C++语言教学中,强调标准是十分必要的。然而市面上的C/C++教材鱼目混珠:那天在师兄的桌上看到的一本国内某知名高校出版的C++教材上,Hello World程序竟有3处明显的问题,不用试验就知道在VS2008或gcc下编译有两处是Error,一处至少是Warning(据说在VC6下可以编译通过)。一些高校计算机教学中普遍使用历史遗留的、非标准的编译器也成为滋生不良编程习惯、阻碍标准推广的源泉。究其原因,某些教师的惰性是一方面,而相关资格考试与现行标准、业界规范的严重脱节也成为应试教育体制下技术标准难以为师生重视的重要因素。正面的例子又如何呢?北理工计算机学院有一门程序设计实践与方法的本科课程,这门课的重心本是算法与编程技巧的训练,但由于它使用了基于gcc的Online Judge机制和像ACM-ICPC一样严格、众多的测试用例,使得学生对代码的标准与严格、算法的正确与高效、条件的完全覆盖必须做仔细的考量。我自己上过这门课,也做过这门课的助教,两种角色的体验让我深知师生各自的抱怨与苦衷。错误究竟在谁?我想不在于这门课的教师和学生,而在于这门课之外的计算机教育氛围。改变这个氛围并不可以一蹴而就,但至少有两点是可以尽己所能的:主观方面,建立标准意识,培养良好的编程习惯;客观方面,使用实现了符合现行标准的编译器。尽管这对计算机教学的整体环境影响甚微,但起码对未来从事计算机方面的工作来说是有益的——实际工程对标准的要求非同儿戏。

相关文章

发表您的评论

您的名字: (必填)

您的邮箱: (不会被公布,必填)

您的网站:

* 正确填写邮箱可支持Gravatar头像服务。
* 与主题无关的内容请用邮件或IM与我联系。