C++ Lite Memo
除了Bajarne Stroustrup本人之外,几乎没有人适合来回顾C++历史。介绍历史是非常严肃而艰难的工作,我个人的力量难以胜任。但是我可以从普通开发人员的角度,谈谈了解C++历史的一些点滴心得。
介绍
诞生于1983年的C++,仍然是一门较新的语言。相比于悠久的Fortran和伟大的Lisp,C++的诞生年代已经是计算机语言蓬勃发展的时期。然而在这短暂的20多年中,C++迅速成长为一个大型的复杂语言。1993年我刚刚接触 C++时,所有的书本上都说 C++是一门面向对象的语言(现在看来,即使在当时这么说也很有问题),而现在的C++已经发展成了一门“多范型(multi-paradim)”的通用语言(general purpose)[wiki]。
作为一个C++的普通使用者和不断的学习者,我将围绕下面3个范型略微谈谈 C++的技术发展历史,它们是 OO 范型、FP 范型和过程范型。当然,C++支持的范型绝不仅有这3个。
OO范型
回溯面向对象的历史,时间可以一直退回到20世纪60年代。有两个重要软件对面向对象技术的诞生产生了深远的影响。其中一个根本就不是编程语言,而是名叫Schechpad的图形编辑器,它是在1963年由麻省理工学院(MIT)的Ivan Sutherland 创作的。据我们所知,Sketchpad 是世界上第一个面向对象的图形编辑器。另外一个软件是一门程序设计语言,它被设计用来更方便地实现仿真。该语言名叫Simula,是1966年在挪威被开发出来的。在Simula中,可以定义一个行为,并且从一个行为可以创建任意多的工作过程。虽然 Simula 仍然是一种通用的过程化编程语言,但是用户已经可以使用它创建一些物体来模拟真实世界了。[squeak]
1966年,这两个软件启发的想法在犹他大学的学生 Alan Kay的头脑中碰撞出了火花,他发现了这两者之间的联系。实际上,他发现了如何建立稳定的大型系统的关键:使用一台计算机的最好办法就好比这台计算机的内部有成千的小计算机——每个都是独立的,但是彼此之间却按照清晰的规则相互作用。
1970年,Alan Kay 加入了施乐(Xerox)公司设在Palo Alto 的新研究中心,他在那里领导学习研究小组(Learning Research Group)发明了Smalltalk。Smalltalk是世界上第一个面向对象语言(至今仍然被公认为最纯粹的面向对象语言),并且不同于早期的Simula,Smalltalk 同现在我们所知道的“面向对象”完全一致。
时光流转到1979年,Bjarne Stroustrup开创了面向对象语言的一条独立的分支。他希望创造出一种非常高效的Simula版本。Bjarne当时在贝尔实验室(Bell Labs)工作,贝尔实验室中曾经诞生了C语言。他开发出了好几个版本的语言,它们很像C,但是具有面向对象的扩展。当时的命名是“C with classes”,1983年,C++诞生了。
C++ 和Smalltalk都来自于Simula的思想,但是它们采用了截然不同的途径。比较下面的代码就可以看出区别。比如下面这段C++代码:
int main(int, char**){ vector<int>coll; for(int i=0;i<10;++i) coll.push_back(rand()%100+1); sort(coll.begin(),coll.end()); copy(coll.begin(),coll.end(),ostream_ iterator<int>(cout,"\n")); }
再看Smalltalk 代码:
|coll| coll:=Array new:10. 1 to:10 do:[:i|coll at:i put: (100 atRandom)]. coll sort. 1 to:10 do:[:i|Transcript show: (coll at:i); cr].
上述两段代码都是产生10个100以内的随机数,放入一个容器并排序输出。Smalltalk 代码中没有任何类型声明,所有的对象都是纯动态的,而C++的代码则展现了静态强类型的特性。所有的对象(变量)在使用前都已经明确规定了类型,只有类型正确才能进行计算或操作。人们很难简单评价这两种设计思路的优劣。在不断发展中,这两种思路都展现出了强大的生命力。前者催生了诸如Python、Ruby 等动态类型语言,而采用 C++这一思想的代表语言则包括Java、C#等。
OO范型是C++自诞生之时就希望达成的设计目标。在C++诞生之后的近20年,正是OO高速发展的20年。软件系统的复杂性超出了人们早期掌握的方法论所能驾驭的范围,而硬件系统的发展尚未像今天这样足以支持动态语言和函数式语言,Internet 也处于很不普及的阶段。Smalltalk受限于Xerox 和IBM等少数业界巨人之中,于是C++几乎成了最理想的廉价面向对象语言。尽管C++语言和面向对象概念自身一直争议和疑议不断,但是在黄金发展的几年内,C++的使用者几乎每7个月增加一倍。在1998年春天,C++在全世界程序设计语言中的占有率达到了不可思议的高峰76%[wiki],人们争相学习 C++。有这么一件事我记忆犹新,1995年我上大一的时候,选修了和专业几乎无关的化学课,主讲老师有一次在课上说,以前我们努力学习 Fortran,现在不成了,要改学C++了。那个时代各种C++的实现存在于各种各样的系统中。提起其中的某些名字,想必脍炙人口,例如著名的Turbo C++、Borland C++及稍后来微软的Visual C++。这么多的C++彼此互有差异,甚至同一公司的C++的不同版本也有所不同。业界也越来越需要一种统一的C++标准。于是1998年ANSI-ISO终于实现了对C++的标准化,这就是著名的C++98标准。
98标准的诞生,是 C++语言的一件大事,从此 C++的各个编译器厂商逐渐统一到这个标准上,“书同文,车同轨”。C++逐渐拥有一些非常贴近标准的优秀的编译器,例如GNU、微软、Borland、Intel 等。然而事情的发展从来都不是顺利的,C++背负的历史包袱越来越沉重。随着Java等新兴事物的出现,业界逐渐发现由于Java相对于C++降低了复杂性,更容易大量招募和培训开发人员,而培养一个优秀的C++开发人员需要更长的时间和更大的投入。在教学领域也是如此,C++的复杂性使它天生不适合作为一门教学语言, Java在各个高校中逐渐取代了C成为流行的教学语言。到2004年秋,C++的占有率已经降低到46%。
然而从技术层面上看,C++98标准仍然树立了一个新高。尤其是其中的标准模板库STL,得到了极高的赞誉,被列为面向对象,泛型编程和设计模式的典范之作。
FP范型
如果10年前说C++ 是函数式编程语言(Functionalprogramming),估计没有多少人会赞成,可是看看下面的阶乘代码:
template<int n> struct Fact{ static const int value = n*Fact<n-1>::value; }; template<> struct Fact<0>{ static const int value = 1; }; int main(int argc, char* argv[]){ std::cout<<"5!="<<Fact<5>::value; }
然后再和主流函数式编程语言Haskell 以及强调FP的Lisp方言Scheme的代码对比。比如Heskell 的阶乘代码:
fact 0 = 1 fact n=n*fact (n-1) Lisp方言Scheme的阶乘代码: (define (fact n) (if (=n 0) 1 (*n (fact (-n 1)))))
比较的结果非常有趣,利用模板编译期计算的C++的阶乘实现和Haskell 的阶乘实现出奇地相似;而C++以模板偏特化的功能,实现了Haskell 中的pattern matching,进而达到了Scheme中的if 功能。更有趣的是,所有这些函数式的计算,完全发生在编译期。一旦编译完成,复杂的计算也就完成了。程序运行时,就可以直接利用编译时的运行结果,大大加快了运行的速度。
上述的代码也许像是个小游戏,最初人们发现 C++的这一功能,是出于某种偶然。有人发现了一个程序的编译错误竟然是一串素数。目前人们已经承认,利用模板推导进行的C++的编译期计算,是图灵完备的。读者可以参考[cpp-mp] 看到一个完全用编译期编程实现的素数筛法。为什么编译期编程会支持 FP 呢?这主要是由于C++的的语言特性决定的:
1.编译时不能使用变量,只能使用常量。这正好和命令式编程相反而符合FP的要求。
2.模板推导允许递归定义,这和FP强调递归相符。
3.模板偏特化,这是一个一举两得的武器,即可以用来定义递归出口,又可以实现类似if 的逻辑判断和分支。
有了这三大特性,C++ template meta programming的概念浮出水面,并为越来越多的人接受。AndreiAlexandrescu 在他的著作《Morden C++ design》中感叹:“这太像LISP了”。Andrei 把C++的 FP 功能充分发挥,实现了型别的表数据结构,并给出了添加、查找、删除的一系列实现。这直接成为了著名的 Loki 库的技术基础。无独有偶,另一个使用 C++FP 范型的著名应用就是boost::mpl[mpl]。现在翻看boost 代码,就会发现mpl 几乎成为了boost 所有库的基础。用户甚至可以直接使用 mpl 给出一个 BNF语法描述,从而像Yacc 一样生成一个DSL语言的编译器。现在,人们通常把2000年至今称为C++语言发展的第3个时代[wiki]。这个时代FP范型成为C++支持的多范型中的一员。随着C++0x 在今年的发布,Lambda 正式成为语言的一员,FP已经不再是编译期计算的专利,它将继续延伸到C++的运行时。
以模板推导为核心的FP范型,彻底把C++ 同Java 与C# 的generic 区别开来。C++ 在型别抽象的基础上超前地迈出了一步,它更接近ML 和Haskell 语言中的“类型系统推导”。回顾这段历史,一个著名的C++库起到了上承OO范型,下启FP范型的关键作用,它就是STL。
STL被很多人认为是世界上最优秀的软件库之一。自从STL 问世后,“泛型编程”就越来越多受到人们的关注。如果说面向对象是对数据和行为的抽象,范型就是对类型系统的抽象。STL的成功,不是偶然的一蹴而就。其作者Alexander Stepanov 自1979年前后,就开始设计构思它,而这也正是Bjarne Stroustrup 创造C++语言的年代。也许有人会问,那个年代还没有 C++,如何设计 STL ?这还要归功于伟大的 LISP,Stepanov 在进行早期的泛型研究时,使用LISP方言Scheme写了大量的程序,后来Stepanov 在使用Ada时,Scheme的动态特性和Ada类型系统的强制特点这些火花碰撞在一起,最终在 C++的模板机制下,得以完美地实现。1994年, STL最终以80%的压倒性多数进入了ISO C++标准,它推动着C++向着泛型编程方向高速发展。
由于STL 的成功,泛型编程被越来越广泛地接受,Java和C#先后引入了generic,以扩充语言对泛型的支持。
过程范型
如果说起C++和C的关系,结论并不像想象的那么简单。如果从历史上看,C 语言无疑是 C++的前驱语言。可是现代绝大多数观点认为,C++和 C 完全是不同的语言。《程序员》曾于2001年4月就此登载过Bjarne的文章。实践发现,没有学习过C语言,而直接学习C++的效果反而更好。C++被很多人认为是C语言的超集,然而实际情况却并非像数学中的集合论那样严谨。实际系统中需要在C++中混用C程序的情况下,extern C声明仍然是强烈推荐的稳妥做法。C++虽然产生在C语言之后,但有很多C++发明的东西,也回馈到C语言之中而被接受,例如//注释,for 循环中声明变量等。
如果探寻这些复杂关系的背后,却有着朴素的原因,那就是既要能够抽象复杂性以应对软件规模越来越大的挑战,又要保证效率以适应当初捉襟见肘的硬件资源。所以在效率仅次于汇编语言的C语言基础上,进行面向对象扩充,就是一种非常自然而然的想法和实用的工程学选择。有数据表明,编写良好的C++程序和完成同样计算的C程序的性能误差在5%之内。
正因如此,过程范型就和 OO 范型一样,是 C++ 自诞生之日起就力图支持的范型。即使在被称为 OO 和设计模式典范的 STL中,这样的例子也随处可见,例如下面的代码[sgi-stl]:
while (__len>0) { __half = __len >> 1; __middle = __first; std::advance(__middle, __half); if (__val<*__middle) __len = __half; else { __first = __middle; ++__first; __len = __len - __half - 1; } } return __first;
这段代码是STL中折半查找算法的核心代码,它在first和last 中查找_val,返回第一个大于这个值的迭代器,或者返回end() 表示没有找到。这段代码和任何面向过程的语言写出的结果毫无二致,即使用标准C写也几乎一模一样。同样,C++语言同C语言一样贴近硬件,饱受争议的指针运算可以方便地操作内存和端口。正是由于支持这些经典的过程范型,C++才得以获得了类似C语言的效率。
其他
截至目前为止,C++0x 的标准仍尚未发表,标准委员会已经承诺将在2009年公布新的标准。GNU的G++和微软等已经先后部分支持了最新 Technical Report 中的一些新特性。近年来,C++的应用环境也逐渐发生了很大的改变。在高速扩张的时代,C++几乎应用到所有领域中。随着硬件水平的飞速发展,今天人们已经有了更多的选择,动态语言,依赖虚拟机的语言,函数式语言等。由于性能已经不再是瓶颈,而软件的复杂性成了大多数问题中的主要矛盾,C++在这些领域中或退出或改变。例如微软的C++/CLI就是典型的改变例子,除了在语言自身中增加托管、GC 等特性之外,编译结果也变成了中间语言代码。微软的 C++被以这种方式纳入到dot Net 体系中来。《程序员》曾于2004年和2005年发表多篇文章对此加以介绍。另一个有趣的例子是Qt,它直接在C++正常编译前,进行一个预处理,利用Meta Object Syetem进行加工,从而模拟出类似Java反射的功能。
而更多的情况是 C++逐渐转移到对性能和硬件有很强限制的应用中。例如系统软件、设备驱动、嵌入式系统、高性能应用、3D游戏等。有趣的是,即使在这些系统中,往往也要对C++进行大量的简化和限制。例如占领智能手机市场65%的Nokia Symbian系统,几乎全部使用C++开发,但是为了适应手机设备中很受限的内存,长年不关机不重启的稳定性,以及在人命关天时能正常拨打119、120等紧急电话的要求,Symbian C++砍掉了标准C++的大量特性,如禁止对象直接构造和拷贝构造,放弃了模板及异常捕捉,而代之以清除栈两步构造、瘦模板和 trap-leave(类似于 setjmp/longjmp),这使得Nokia 能够使用相对较差的硬件而构建出性能优异稳定的手机。
C++的复杂性是一把双刃剑,Linus Torvalds 曾经激烈批评C++语言,这样的声音并非无端指责。以Linux 内核为代表的C语言系统的极大成功,证明了简单一致的重要性。作为本文的结束,我们以图灵奖得主Alan Perlis 在给SICP的序言里的话与C++程序员们共勉:“绝不要认为似乎成功计算的钥匙就掌握在你的手里。你所掌握的,也是我认为并希望的,也就是智慧。”[sicp] █