前言(1)
差不多有10年之久,我在贝尔实验室(Bell Laboratories)埋首于C++(www.cppentry.com)的实现任务。最初的工作是在cfront上面(Bjarne Stroustrup的第一个C++(www.cppentry.com)编译器),从1986年的1.1版到1991年9月的3.0版。然后移转到Simplifier(这是我们内部的命名),也就是Foundation项目中的C++(www.cppentry.com)对象模型部分。在Simplifier设计期间,我开始酝酿本书。
Foundation项目是什么?在 Bjarne 的领导下,贝尔实验室中的一个小组探索着以C++(www.cppentry.com)完成大规模程序设计时的种种问题的解决之道。Foundation项目是我们为了构建大系统而努力定义的一个新的开发模型(我们只使用C++(www.cppentry.com),并不提供多重语言的解决方案)。这是个令人兴奋的工作,一方面是因为工作本身,一方面是因为工作伙伴:Bjarne、Andy Koenig、Rob Murray、Martin Carroll、Judy Ward、Steve Buroff、Peter Juhl,以及我自己。Barbara Moo管理我们这一群人(Bjarne和Andy除外)。Barbara Moo常说管理一个软件团队,就像放牧一群骄傲的猫。
我们把Foundation想象成一个核心,在那上面,其他人可以为使用者铺设一层真正的开发环境,把它整修为他们所期望的UNIX或Smalltalk模型。私底下我们把它称为Grail(传说中耶稣最后晚餐所用的圣杯),人人都想要,但是从来没人找到过!
Grail使用一个由Rob Murray发展出来并命名为ALF的面向对象层次结构,提供一个永久的、以语意为基础的表现法。在Grail中,传统编译器被分解为数个各自分离的可执行文件。parser负责建立程序的ALF表现法。其他每一个组件(如type checking、simplification、code generation)以及工具(如browser)都在程序的一个ALF表现体上操作(并可能加以扩展)。Simplifier是编译器的一部分,处于type checking和code generation之间。Simplifier 这个名称是由Bjarne所倡议的,它原本是cfront的一个阶段(phase)。
在type checking和code generation之间,Simplifier做什么事呢?它用来转换内部的程序表现。有三种转换风味是任何对象模型都需要的:
1.与编译器息息相关的转换(Implementation-dependent transformations)
这是与特定编译器有关的转换。在ALF之下,这意味着我们所谓的"tentative"nodes。例如,当parser看到这个表达式:
fct();
它并不知道是否(a)这是一个函数调用操作,或者(b)这是overloaded call operator在class object fct上的一种应用。默认情况下,这个式子所代表的是一个函数调用,但是当(b)的情况出现,Simplifier 就要重写并调换 call subtree。
2.语言语意转换(Language semantics transformations)
这包括constructor/destructor的合成和扩展、memberwise初始化、对于memberwise copy的支持、在程序代码中安插conversion operators、临时性对象,以及对constructor/destructor的调用。
3.程序代码和对象模型的转换(Code and object model transformations)
这包括对virtual functions、virtual base class和inheritance的一般支持、new和delete运算符、class objects所组成的数组、local static class instances、带有非常量表达式(nonconstant expression)之global object的静态初始化操作。我对Simplifier所规划的一个目标是:提供一个对象模型体系,在其中,对象的实现是一个虚拟接口,支持各种对象模型。
最后两种类型的转换构成了本书的基础。这意味着本书是为编译器设计者而写的吗?不是,绝对不是!这本书是由一位编译器设计者针对中高级C++(www.cppentry.com)程序员所写的。隐藏在这本书背后的假设是,程序员如果了解C++(www.cppentry.com)对象模型,就可以写出比较没有错误倾向而且比较有效率的代码。
什么是C++(www.cppentry.com)对象模型
有两个概念可以解释C++(www.cppentry.com)对象模型:
1. 语言中直接支持面向对象程序设计的部分。
2. 对于各种支持的底层实现机制。
语言层面的支持,涵盖于我的C++(www.cppentry.com) Primer一书以及其他许多C++(www.cppentry.com)书籍当中。至于第二个概念,则几乎不能够于目前任何读物中发现,只有[ELLIS90]和[STROUP94]勉强有一些蛛丝马迹。本书主要专注于C++(www.cppentry.com)对象模型的第二个概念。本书语言遵循C++(www.cppentry.com)委员会于1995冬季会议中通过的Standard C++(www.cppentry.com)草案(除了某些细节,这份草案应该能够反映出此语言的最终版本)。
C++(www.cppentry.com)对象模型的第一个概念是一种"不变量"。例如,C++(www.cppentry.com) class的完整virtual functions在编译时期就固定下来了,程序员没有办法在执行期动态增加或取代其中的某一个。这使得虚拟调用操作得以快速地派送(dispatch)结果,付出的成本则是执行期的弹性。
对象模型的底层实现机制,在语言层面上是看不出来的--虽然对象模型的语意本身可以使得某些实现品(编译器)比其他实现品更接近自然。例如,virtual function calls,一般而言是通过一个表格(内含virtual functions地址)的索引而决议得知。一定要使用如此的 virtual table 吗?不,编译器可以自由引进其他任何变通做法。如果使用virtual table,那么其布局、存取方法、产生时机以及数百个细节也都必须决定下来,而所有决定也都由每一个实现品(编译器)自行取舍。不过,既然说到这里,我也必须明白告诉你,目前所有编译器对于virtual function的实现法都是使用各个class专属的virtual table,大小固定,并且在程序执行前就构造好了。
如果C++(www.cppentry.com)对象模型的底层机制并未标准化,那么你可能会问:何必探讨它呢?主要的理由是,我的经验告诉我,如果一个程序员了解底层实现模型,他就能够写出效率较高的代码,自信心也比较高。一个人不应该用猜的方式,或是等待某大师的宣判,才确定"何时提供一个copy constructor而何时不需要"。这类问题的解答应该来自于我们自身对对象模型的了解。
写本书的第二个理由是为了消除我们对于C++(www.cppentry.com)语言(及其对面向对象的支持)的各种错误认识。下面一段话节录自我收到的一封信,来信者希望将C++(www.cppentry.com)引进于其程序环境中:
我和一群人工作,他们过去不曾写过(或完全不熟悉)C++(www.cppentry.com)和OO。其中一位工程师从1985就开始写C了,他非常强烈地认为C++(www.cppentry.com)只对那些user-type程序才好用,对server程序却不理想。他说如果要写一个快速而有效率的数据库引擎,应该使用C而非C++(www.cppentry.com)。他认为C++(www.cppentry.com)庞大又迟缓。
C++(www.cppentry.com)当然并不是天生地庞大又迟缓,但我发现这似乎成为C程序员的一个共识。然而,光是这么说并不足以使人信服,何况我又被认为是C++(www.cppentry.com)的"代言人"。本书就是企图极尽可能地将各式各样的Object facilities(如inheritance、virtual functions、指向class members的指针……)所带来的额外负荷说个清楚。
除了我个人回答这封信外,我也把此信转寄给HP的Steve Vinoski;先前我曾与他讨论过C++(www.cppentry.com)的效率问题。以下节录自他的回应:
过去数年我听过太多与你的同事类似的看法。许多情况下,这些看法是源于对C++(www.cppentry.com)事实真相的缺乏了解。在上周,我和一位朋友闲聊,他在一家IC制造厂服务,他说他们不使用C++(www.cppentry.com),因为"它在你的背后做事情"。我连连追问,于是他说根据他的了解,C++(www.cppentry.com)调用malloc()和free()而不让程序员知道。这当然不是真的。这是一种所谓的迷思与传说,引导出类似于你的同事的看法……
在抽象性和实际性之间找出平衡点,需要知识、经验以及许多思考。C++(www.cppentry.com)的使用需要付出许多心力,但是我的经验告诉我,这项投资的回报率相当高。
我喜欢把本书想象成是我对那一封读者来信的回答。是的,本书是一个知识陈列库,帮助大家去除围绕在C++(www.cppentry.com)四周的迷思与传说。
如果C++(www.cppentry.com)对象模型的底层机制会因为实现品(编译器)和时间的变动而不同,我如何能够对于任何特定主题提供一般化的讨论呢?静态初始化(Static initialization)可为此提供一个有趣的例子。
已知一个class X有着constructor,如下面所示:
- class X
- {
- friend istream&
- operator>>( istream&, X& );
- public:
- X( int sz = 1024 ) { ptr = new char[ sz ]; }
- ...
- private:
- char *ptr;
- };
而一个class X的global object的声明,如下面所示:- X buf;
-
- int main()
- {
- // buf 必须在这个时候构造起来
- cin >> setw( 1024 ) >> buf;
- ...
- }
C++(www.cppentry.com)对象模型保证,X constructor将在main()之前便把buf初始化。然而它并没有说明这是如何办到的。答案是所谓的静态初始化(static initialization),实际做法则有赖开发环境对此的支持属于哪一层级。
原始的cfront实现品不单只是假想没有环境支持,它也假想没有明确的目标平台。唯一能够假想的平台就是UNIX及其衍化的一些变体。我们的解决之道也因此只专注在UNIX身上:我们使用nm命令。CC命令(一个UNIX shell script)产生出一个可执行文件,然后我们把nm施行于其上,产生出一个新的.c文件。然后编译这个新的.c文件,再重新链接出一个可执行文件(这就是所谓的munch solution)。这种做法是以编译器时间来交换移植性。
接下来是提供一个"平台特定"解决之道:直接验证并穿越COFF-based程序的可执行文件(此即所谓的 patch solution),不再需要nm、compile、relink。COFF 是Common Object File Format的缩写,是System V pre-Release 4 UNIX系统所发展出来的格式。这两种解决方案都属于程序层面,也就是说,针对每一个需要静态初始化的.c文件,cfront 会产生出一个sti函数,执行必要的初始化操作。不论是patch solution还是munch solution,都会去寻找以sti开头的函数,并且安排它们以一种未被定义的顺序执行(由安插在main()之后第一行的一个library function _main()执行之)(译注:本书第6章对此有详细说明)。
System V COFF-specific C++(www.cppentry.com)编译器与cfront的各个版本平行发展。由于瞄准了一个特定平台和特定操作系统,此编译器因而能够影响链接器特地为它修改:产生出一个新的.ini section,用以收集需要静态初始化的objects。链接器的这种扩充方式,提供了所谓的environment-based solution,那当然更在program-based solution层次之上。
至此,任何以cfront program-based solution为基础的一般化(泛型)操作将令人迷惑。为什么?因为C++(www.cppentry.com)已经成为主流语言,它已经接收了越来越多的environment-based solutions。本书如何维护其间的平衡呢?我的策略如下:如果在不同的C++(www.cppentry.com)编译器上有重大的实现技术差异,我就至少讨论两种做法。但如果 cfront之后的编译器实现模型只是解决cfront原本就已理解的问题,例如对虚拟继承的支持,那么我就阐述历史的演化。当我说到"传统模型"时,我的意思是Stroustrup的原始构想(反映在cfront身上),它提供一种实现模范,在今天所有的商业化实现品上仍然可见。