1、 标准维护:1997-2005
在标准通过之后,ISO进程进入了“维护模式”差不多5年。C++委员会决定这样做因为:
·委员会的成员累了(对于某些成员来说,工作了10年),想去做其它事情
·社团还没有理解新的特征
·许多实现者还不支持新的特征、库和工具
·没有让委员们或社团感到紧急的新的伟大的想法。
·对于许多成员,用于标准的资源(时间和金钱)太少
·许多成员(包括我)认为ISO进程需要一个“冷却期”
在“维护模式”,委员会主要是对缺陷报告做出回应。大多数缺陷通过文字得以澄清或解决矛盾。只有少数的新的规则被引入,真正的创新被避免。稳定就是目标。在2003年,所有的这些小的修正以“技术勘误表1”的名义发表了。同时,英国委员会把握这次机会去矫正一个长期存在的问题:他们说服Wiley去发行修正版本的标准[66]。最初的和许多艰难的工作都由Francis Glassborow和Lois Goldthwaite完成,委员会的项目编辑Andrew Koenig,他创作了现行的文本,也给予了技术支持。
直到2003年修正标准出版,公众所能获取的惟一的标准是非常昂贵的(大约$200)来自ISO的纸质拷贝或来自INCITS(前身是ANSI X3)的另一种pdf版本。pdf版本在当时是一种新奇的事物。标准机关通过贩卖这些标准得到部分的财政支持,因此他们最不情愿去使得标准免费或很便宜。另外,他们没有零售的渠道,所以你在当地的书店中找不到国家或国际标准—当然除了C++标准。象C++一样,C标准现在也可得到了。
许多最好的标准工作对于平常的程序员来说是看不到的,并且是相当的深奥,当陈述出来时又是无聊的。许多工作用在寻找更清楚的表达方法和“每个人都知道,但就是用书面语言表达不出来”和解析模糊不清的事情—至少理论上—不会影响到大多数程序员。维护本身大部分就是“无聊和深奥”的事情。更进一步,委员会需要关注标准在某处自身矛盾或看起来是自身矛盾的事情。然而,这些事情对于实现者来说是十分重要的,确保一个特定的语言使用被正确地处理。反过来,这些事情对程序员来说也是十分重要,因为尽管最小心地编写代码,大的程序也会故意地或意外地依赖于某些特征,对某些人来说,那会变得难解和深奥。除非编译器实现者同意,程序员要获得可移植性将变得困难并且容易成为单一编译器提供商的人质—而那有背于我对C++应该是什么的观点。
给一个这项维护工作量的数字(不明确地开展下去):从1998年标准完成到2006年,core和库工作组每个处理超过600个“缺陷报告”。幸运的是,不是所有的报告都是有缺陷的,但要确定那真的不是问题或这个问题只是由于在标准中没有表述,需要时间和注意。
维护并不是委员会从1997到2003的全部工作,有一个大小适度的计划用于将来(考虑C++0x),但主要工作是写一个关于性能问题[67]的技术报告和一个关于库的报告[68].
6.1 性能技术报告
性能技术报告(“TR”)[67]是由一个建议标准化C++的分支用于嵌入式系统编程而促进产生的。提议称之为嵌入式C++[40]或简单的EC++,源于日本嵌入式系统工具开发者协会(包括Toshiba,Hitachi,Fujitst和NEC),有两个主要的关注点:移除对性能有潜在伤害的语言特征和移除对于程序员来说太过复杂的语言特征(这被认为是一个潜在的效率和正确性危害)。一个没有明确阐明的目标是去定义一些比标准C++更容易实现的短术语。
在这个C++子集中禁止的特征包括:多继承、模板、异常、运行时类型信息(§ 5.1.2 )、新风格的转换、名字空间。对于标准库,STL和locale也被禁止,并且提供了iostream的替代版本。我认为这个提议误入歧途并且是倒退的。特别地,性能代价是假想出来的,比如,模板的使用已被多次证明是对嵌入性系统的性能(时间和空间)和正确性是关键的。然而,在1996年EC++被第一次提议时,并没有多少的数据能够说明这一点。讽刺的是,今天少数使用EC++中的大多数使用“扩展EC++”[36],即是EC++加模板。类似的,名字空间(§5.1)和新风格的转换(§5)主要用于澄清代码,并能用于降低维护的难度和验证的正确性。最好的证明(并常常被引用的)是完整的C++与EC++的比较是iostream。主要原因是C++98的iostream支持locale而老的iostream不支持。这就有些讽刺意味因为locale被加进来是为了支持不同于英语的语言(比如日语),能用于优化掉其它没有使用到的环境([67])。
在认真的考虑和讨论后,ISO委员会决定坚守长期以来的不支持方言的传统—尽管那只是分支。每个方言都导致使用者社团的分裂,甚至正式定义这样分支也会造成这样的破坏,当它用户开始去发展一种单独的技术、库和环境的文化。不可避免地,关于与语言相关的子集的失败故事开始出现。因此,我建议反对EC++的使用,而是使用完整的ISO标准C++。
显然,那些提议EC++的人们在想得到一个有效率的、容易实现和相对容易使用的语言,这些想法都是正确的。委员会需要示范ISO标准C++就是这样的一个语言。特别地,对于委员会来说,对被EC++出于性能关键、资源受限、安全关键的任务的原因而反对的特征的使用进行文档化看起来是一项合适的任务。所以决定去写一份关于性能的技术报告,它的实行总结如下:
“这份报告的目标是:
·给读者一个使用各种C++语言和库特征所带来的时间和空间开销的模型
·揭穿流传广泛的有关性能问题的流言
·展示用于关注性能的应用程序中的C++技术
·展示用于实现C++标准语言和库设施的技术,用于产生高效的代码
至于运行时和空间性能的关注,如果你能够在应用程序中使用C语言开发,那么你也能够用C++去开发。”
在我们需要某些高性能和嵌入式应用的场合中,不是每一个C++特征都是高效和可预测的。如果我们可以预先容易和精确地确定每个特征使用所需要的时间,那么这样的特征是可预测的。在嵌入式统的环境中,我们必须考虑如果我们能够使用:
·自由存储区(new/delete)
·运行时类型信息(dynamic_cast和typeid)
·异常(throw/catch)
要完成这些操作的时间依赖于代码的环境(比如,在throw到达跟它匹配的catch之前有多少个栈需要unwinding)或程序的状态(比如,new的时序和在new之前的delete)。
用于嵌入式或高性能应用的编译器都有编译选项用于禁止运行时类型信息和异常。自由存储的使用也很容易避免。所有其它C++语言特征是可预测的并且能够被优化掉(根据0开销原则 §2)。甚至异常跟其替代品[93]相比,也是高效的,并且应该能被所有的系统所使用,除了最严格的硬实时系统。TR讨论这些事情并定义一个接口到硬件的最底层(比如寄存器)。TR的工作是由一个工作组—主要由那些关注嵌入式系统,包括EC++技术委员会的成员们所组成的—编写完成的。我在性能工作组中也很积极,并起草了相当部分的TR文档,但主席和编辑首先是Martin O'Riordan,然后是Lois Goldthwaite。致谢表中有28个人,包括Jan Kristofferson,Dietmar Kühl,Tom Plum和Detlef Vollmann。在2004年,关于TR的投票被一致通过。
2004年,在TR被定下来后,来自Lockheed-Martin Aero的Mike Gibbs发现一个算法,允许dynamic_cast能够在常量时间内实现并且速度很快[48]。这就给dynamic_cast最终在硬实时编程中变得可用带来了希望。
TR的工作是少数的几个地方之一,在嵌入式系统中的广大的C++用法变成了公开的可访问的著作。使用范围从高端系统比如电信系统到底层系统,在那里完全和直接访问特定硬件特征是十分重要的(§7)。为服务后者,TR的工作包含一个“硬件寻址接口”,还有它的使用方针。这个接口主要是由Jan Kristofferson(代表Ramtex Internal和丹麦)和Detlef Vollmann(代表Vollmann Engineering Gmbh和瑞士)来完成的。为了让大家先一睹为快,这儿给出的代码的功能是拷贝一个称为PortA2_T的端口所指向的寄存器缓冲的内容:
unsigned char mybuf[10];
register_buffer
for (int i = 0; i != 10; ++i)
{
mybuf[i] = p2[i];
}
本质上以下的操作也能实现块的读操作:
register_access
UCharBuf myBlock;
myBlock = p3;
注意到使用模板和使用整数作为模板的实参;对于库来说这是十分重要的,用于在时间和空间上对性能的优化维护。这对许多乐于听到由于模板所导致的代码膨胀故事的人来说是一个意外。模板通常会对每一种特化产生一份代码拷贝,即对于每一种模板参数的组合。因此,显然,如果你的代码是由模板产生的,并且占用了很多内存,那么你可以使用这些内存。然而,许多现代模板是基于这样的一个观察而写的,即内联函数有可能跟函数的前导一样小,甚至更小。这样的话,你就可以同时节省时间和空间。除了好的内联之外,从模板代码那里还能得到其它好处:没有用到的代码不会被产生和避免假指针。
标准要求除非成员函数被一组特定的模板参数所调用,否则类模板不会产生这样的成员函数。这就自动消除了所有可能变成的“死代码”。比如:
template
public:
void f() { /* ... */ }
void g() { /* ... */ }
// ...
};
int main()
{
X
xi.f();
X
xd.g();
return 0;
}
对于这个程序,编译器必须产生X
另一个需要遵循的简单的规则是为了从模板中获得好的内存性能:“不要使用指针如果你不需要它们”。这个规则保存完整的类型信息,允许优化器去更好地执行(特别是当使用到内联函数时)。这意味着把简单对象作为参数的函数模板应该是通过传值而不是传引用。注意到指向函数和虚函数的指针会破坏这个规则,会给优化器带来问题。
要获得代码膨胀,你只需要这么做:
1、使用大的函数模析(这样产生的代码就会很大)
2、使用许多指向对象的指针、虚函数和指向函数的指针
3、使用“特征非富”的类层次(产生许多潜在的死代码)
4、使用差劲的编译器和差劲的优化器
我设计模板专门是为了让它更容易地为程序员避免(1)和(2)。基于经验,标准也能避免(3),除非你违反(1)或(2)。在90年代初,(4)变成了一个问题。Alex Stepanov命名它为“抽象的惩罚”问题。他定义“抽象的惩罚”为一种比例,即运行该模板操作所需要的时间与实例化该模板操作所需要时间的比例(比如,find在一个vector
C++支持嵌入式系统编程的其它方面—也是同样重要的方面—是它的计算和内存模型是现实世界中硬件的反映:内建类型直接映射到内存和寄存器,内建操作直接映射到机器操作,用于数据结构的组合机制并没有增加间接性或内存的开销[131]。在这一点上,C++与C一样。参见§2。
把C++看成是一种更接近机器语言并带有抽象设施,可用于表达可预测的类型安全的低层的设施,已经变成了Lockheed-Martin Aero用于安全关键硬实时代码的编码标准[157]。我帮忙起草了这个标准。通常,使用C和汇编语言程序员理解将语言设施直接映射到硬件,但那通常不能满足抽象机制和强类型检查的需求。反过来,由高级“面向对象”语言过来的程序员常常看不到需要与与硬件靠得更近的需求,并且期望一些未指定的技术去帮他们传达他们的想要的抽象而又不需要付出不可接受的开销。
6.2 TR库
1997年当我们完成标准时,我们都觉得标准库就是我们认为最迫切需要的也是我们可以提供的简单的库。还有几个也很想要的库,比如hash table、正则表达式、目录操作和线程,都缺失了。由Matt Austern(最开始和Alex Stepanov一起在SGI工作,然后在AT&T和我一起工作,现在在google工作)带领的库工作组马上为这样的一个库开展工作。在2001年,委员会开始了在库技术报告的工作。在2004年,TR库中指定的库被人们认为最迫切需要而被一致投票通过。
尽管标准库和它的扩展的重要性是非常巨大的,我在这里只能简要地列出新的库:
·多态函数对象封装器
·Tuple类型
·数学的特殊函数
·类型粹取
·正规表达式
·增强的成员指针适配器
·通用的智能指针
·引用封装器
·用于计算函数对象返回类型 的统一的方法
·增强的binder
·hash table
在投票的当时,每一个库已经有相应的原型或相应的具有工业强度的实现。他们希望在每一个新的C++编译器中都带有这样的库。很多新库的设施很明显是“技术的”,即,它存在主要是为了支持库的创建者。特别地,它们的存在用于支持STL传统中的标准库设施的创造者。这儿,我只是强调为大多数应用构建者也喜欢的三个库:
·正则表达式
·通用智能指针
·hash table
正则表达式匹配是脚本语言和许多文本处理的支柱。最后,C++有一个标准库用于它。主要的类是regex,提供正则表达式匹配,与ECMA脚本模式兼容并有其它流行的符号。
“智能指针”的主体是一个引用计数指针,shared_ptr,用于需要共享所有权的代码。当指向一个对象的最后一个shared_ptr被销毁时,由它所指向的对象也就被删除。智能指针是流行的,但不普遍,因此对于它们性能的关注和可能的滥用就使得智能指针的祖先counted_ptr被排除在C++98之外。智能指针也不是万能药,特别地,它们比使用普通指针的代价高得多,对象的析构函数由一组shared_ptr所拥有,并且将在不可预测的时候运行。并且如果由于最后一个shared_ptr被删除导至许多对象立刻删除,那么你会招致“废料收集器延迟”,就好象你正在运行一个一般的收集器。这个代价主要跟计数对象所分配的自由存储有关,特别是在线程系统中对使用读数器所加的锁(“lock-free”在这儿能有所帮助)。如果它就是你想要的废料收集器,那么你最好能够简单地使用现在可用的废料收集器[11,47]或等到C++0x(§5.4)。
对于hash table则没有这样的忧虑,它们本应该在C++98中出现,如果我们有时间去做一个恰当的详细的设计和规范说明的话。毫无疑问,在用于大的表时,hash_map是map的另一种替代,其中key是字符串,我们可以设计出好的hash函数。在1995年,Javier Barreirro,Robert Fraley和David Musser尝试及时准备一份提议用于标准化,他们的工作变成了后来的hash_map[8]的基础。委员会没有足够的时间,最后在TR库中,unordered_map(和unordered_set)是8年的实验和工业应用的结果。一个新的名字,选择“unordered_map”是因为有许多hash_map正被使用。unorder_map是与hash_map实现者保持一致的结果。unordered_map是无序的是指遍历元素的迭代器并不需要保证以任何特定次序:hash函数没有定义map的<应该是以什么次序进行。
开发者们对这些扩展的最普通的反应是:“那是时间的问题,为什么它花了你们这么久?”和“我现在需要更多”。那是可以理解的(我现在也想要更多—但是我知道我没有办法获得它),但这样的陈述反应了对ISO委员会是什么及能做什么缺少理解。委员会是靠自愿者才得以运作,并要求规范中达到一致性和不寻常等级的精确的要求(参见D&E §6.2)。委员会没有足够的钱象商用提供商那样为他们的客户花在“免费”和“标准”库。
2、 C++在现实世界中的使用
关于编程语言的讨论典型地都集中在对语言特征的讨论:语言有些什么样的特征?它们的效率怎么样?更多具有启迪性的讨论集中在更难的问题上:语言怎么使用?能被怎么用?谁用它?比如,在早期的OOPSLA,Kristen Nygaard(Simula和OOP)察觉:“如果我们创建了一门需要MIT的PhD才能使用的语言,那么我们是失败的”。在工业环境中,第一个—并且经常只有一个—问题是:谁使用这个语言?为了什么?谁支持它?可替代的语言是什么?本段从C++使用的角度展示C++和C++的历史。
C++在哪里使用?考虑到用户的数量(§1),显然它在很多地方被使用。但由于大多数使用是商业的,这很难有记录。这也是由于C++社团中缺少中央组织带来的缺陷之一—没有人系统地收集关于C++的使用信息,也没有人有接近完整的信息。为了对C++的应用范围有个印象,下面是几个例子,在主要系统和应用中,C++作为关键性的组件:
· Adobe- Acrobat, Photoshop, Illustrator, ...
· Amadeus- airline reservations
· Amazon- e-commerce
· Apple - iPod interface, applications, device drivers, finder, ...
· AT&T-1-800 service, provisioning, recovery after network failure, ...
· Games- Doom3, StarCraft, Halo, ...
· Google- search engines, Google Earth, ...
· IBM - K42 (very high-end operating system), AS/400,
...
· Intel- chip design and manufacturing, ...
· KLA-Tencor- semiconductor manufacturing
· Lockheed-Martin Aero-airplane control (F16, JSF), ...
· Maeslant Barrier- Dutch surge barrier control
· MAN B&W-marine diesel engine monitoring and fuel injection control
· Maya- professional 3D animation
· Microsoft - Windows XP, Office, Internet explorer, Visual Studio, .Net, C# compiler, SQL, Money, ...
· Mozilla Firefox- browser
· NASA/JPL - Mars Rover scene analysis and autonomous driving, ...
· Southwest Airlines - customer web site, flight reservations, flight status, frequent flyer program, ...
· Sun-compilers, OpenOffice, HotSpot Java VirtualMachine,
...
· Symbian-OS for hand-held devices (especially cellular phones)
· Vodaphone - mobile phone infrastructure (including billing and provisioning)
更多的例子,参见[137]。一些最广泛使用和最有利可图的软件产品也曾在这个列表上。无论是C++的理论的重要性和影响,它都满足了它的大多关键设计目标:它成为一个非常有用的现实的工具。它带来面向对象编程,而且最近又把泛型编程引入了主流。在应用程序的数目和应用程序的领域上,C++的使用超过了任何个体的专业范围。那证实我在D&E[121](第四章)所强调的一般性。
这是最不幸的事实,应用程序没能触动研究者、教师、学生和其它应用程序建造者的意识,其记录是不完整的。有许多没有记载或访问不到的经验,这不可避免地歪曲了人们对现实的感觉—关于什么是新的(或认为是新的)的方向上和在出版的文章、学术论文和教科书的所描述的方向。许多可以见到的信息容易受到商业操作。这导致许多“重新发明轮子”,不理想的实践和虚构的故事。
一个普通的虚构故事是“大多C++代码只是使用C++编译器编译的C代码”,对于这样的代码来说没有什么不对—毕竟C++编译器比C编译器能找到更多的bug—并且这样的代码是普遍的。然而,从许多商业的C++代码和与无数的开发者的交谈及从开发者的交谈中,我知道许多主要的应用软件,比如上面所提到的,使用C++是有些“冒险的”。许多开发者都提到在局部代码使用到设计类层次,STL和使用来自STL的想法。参见§7.2。
我们能否根据C++的使用领域进行分类?下面是其列表:
·使用系统组件的应用程序
·银行和财务(转帐、经济模型、客户交互、柜员机等)
·经典的系统编程(编译器、操作系统、编辑器、数据库系统...)
·传统的小的商业应用(物品清单系统、客户服务..)
·嵌入式系统(仪器、照相机、手机、唱片控制器、飞机、电饭煲、医疗系统...)
·游戏
·GUI—iPod,CDE桌面、KDE桌面、Windows,...
·图形
·硬件设计和检验[87]
·底层的系统元件(设备驱动、网络层...)
·科学和数值计算(物理、工程、仿真、数据分析...)
·服务器(web服务器,大应用程序的骨干、计费系统...)
·符号操作(几何学模型、视觉、语音识别...)
·电信系统(电话、网络、监控、计费、操作系统...)
这只是一个列表而不是分类。编程世界抵抗有用的分类。不管怎么样,通过这个列表,有一个事实是明摆的,C++不能成为所有东西的最后目标。事实上,从早期的C++,我就已经认定在定义明确的应用领域,通用语言至多是第二好的。而C++是一门偏向系统编程的通用编程语言。
7.1 应用编程vs系统编程
考虑这么一个似是而非的论点:C++有一点偏向系统编程但大多数的程序员都在写应用程序。显然,数以百万的程序员是在写应用程序,许多用C++写他们的应用程序。为什么?为什么他们不用应用程序编程语言写应用程序呢?对于“应用程序编程语言”的定义有许多种方法,我们可以把“应用程序编程语言”定义为一个我们不能直接访问硬件,而对重要的应用提供直接和特殊的支持的概念(比如数据库访问,用户互动或数值计算)。然后,我们可以把这些应用程序认为是“应用程序语言”:Excel、SQL、RPG、COBOL、Fortran、Java、C#、Perl、Python等,不管是好是坏,当需要比较专业的领域(更安全、更容易使用、更容易优化等),C++被当作通用编程语言。
由于还不至于那么无知,我没有声称C++是接近完美(那将会是可笑的),也没有说它不需要改进(毕竟我们正在C++0x上工作,参见§9.4)。然而,C++有一个合适的职务—一个非常大的职务—那是当前其它语言无法担任的:
·带系统编程的应用程序;通常是受资源限制
·应用程序由不同应用领域的组件组成,以致没有一个单一的特定的应用语言能支持全部
应用程序通过它们的特殊性获得获得它们的优势,通过增加便利和降低使用的难度或潜在的危险特征。通常,那都有运行时或空间的代价。通常,简化是基于执行环境的假设。如果你正好需要一些基本的东西,那些是在设计中被认为是不必要的(比如直接访问内存或完全普遍的抽象机制)或不需要“增加的便利”(并且不能负担起它们强加的开销),那么C++就变成了一个候选者。对许多由C++创建的应用程序来说,它们都有许多组件,对这些组件的基本的猜想都是这种情况:“某些时候,我们中的大多数做与众不同的事”。
正确陈述的方法是一般机制打败了大多数应用程序的特殊用途的特征,这些应用程序不能进行独特的分类,必须与其它应用程序合作或从它们原来狭窄的小生境中演化。这是每一个语言增加通用语言特征的原因之一,无论语言原来的目标是什么以及它所要阐述的哲学是什么。
如果从应用程序语言到通用语言的转变是容易且代价低的话,我们将会多一个选择。然而,很少有这样的情况,特别是当有性能要求或需要机器级的访问时。特别地,使用C++你可以(而不是必须)打破一些基本的那些应用程序建立起来的假设。实践的结果是当你在应用程序中需要用到系统编程或需要C++的性能关键的设施时,在应用程序的大部分中使用C++会让这些变得很方便,然后C++的高层(抽象)设施也很也用。C++提供几乎提供了能够直接在应用程序中应用的高层特征,它所提供了用于定义这些设施成为库的机制。
从历史的观点看这个分析不需要是正确的或是事实的惟一的可能解释。许多人喜欢从另外的角度看待问题。不同的观点对语言和公司的成功有不同的定义。然而,C++的设计是基于这种观点,并且这种观点引导了C++的演化。比如,参见第9章[121]。我认为这是C++在主流中开始成功的理由,并且这个理由让C++持续稳定增长,尽管在市场上不断地出现良好设计和有更好资金支持的替代品。
7.2 编程风格
C++支持好几种编程风格,有时它们也被称之为“范例编程”。不管怎么称呼,C++通常被指为“面向对象编程的语言”。但只有当歪曲“面向对象”的定义时这才是真的。我从来没有这么说“C++是一个面向对象语言”[122],我更喜欢“C++支持面向对象编程和其它编程风格”或“C++是一个多范例编程语言”。因此,人们提到语言问题是因为它设置了期望并影响了人们看法。
C++几乎把C(C89)作为一个“子集”,支持在C中普遍使用的编程的风格。有争议的,C++通过提供更多的类型检查和更多的符号支持,比C更好地支持了那些风格。因此,许多C++代码是用C的风格编写—更普遍的—以C的风格的代码、类和类层次混在一起不会影响整体设计。这些代码从根本上是面向过程的,使用类去提供一组更丰富的类型。有时被称之为“带类的C风格”。那种风格比纯C要好得多(用于理解、调试和维护等)。然而,从历史的观点看,这样比那些使用C++设施去表达更多高级编程技术的代码无趣得多,而且通常效率比较低下。由纯粹的“C风格”完成的C++代码在过去的15年已经越来越少。
C++的抽象数据类型和面向对象的风格的使用已被讨论得够多了,这儿不再需要进一步解释(比如,看[126])。它们是许多C++应用程序的支柱。然而,对于这些实用程序也有它们的限制。比如,面向对象编程会导致过度依赖严格的等级和虚函数。还有,当你需要在运行时选择执行某个动作时,虚函数的调用是基本还是有效率的,但它仍然是一个间接的函数调用,因此同单一机器指令相比,仍然是开销巨大的。这导致在要求高性能的地方,泛型编程变成了C++的标准选择。
7.2.1 泛型编程
有时,C++只是简单地把泛型编程定义为“使用模板”。那最多也只是一种简化。从一个编程语言特征的观点来看,更好的描述是“参数多态”[107]和基于参数选择动作和构建类型的重载。模板是一种基本的编译期机制,用于产生基于类型参数 、整数参数等的定义(类和函数) [144]。
在模板之前,C++中的泛型编程通过宏[108]、 void*和转换或抽象类实现。自然,有些仍然在被使用,有时这些技术也有优势(特别是当与模板化接口结合时)。然而,泛型编程的当前主宰形式依赖于用于定义类型的类模板和用于定义操作(算法)的函数模板。
基于参数化,泛型编程天生就比面向对象编程更规则。从多年的对主要泛型库的使用中,比如STL,可以得出这么一个主要结论,当前C++对泛型编程的支持是不够的,C++0x关注这部分的问题(§8)。
按照Stepanov(§ 4.1.8 )的意思,我们可以定义泛型编程而不需要提及语言特征:从具体的例子提升算法和数据结构到它们最一般的和抽象的形式。这就暗示着把算法和它们对数据的访问用模板表示,正如对STL的描述中所展示的那样。
7.2.2 模板元编程
C++模板实例机制是(当编译器限制可以被忽视,正如它们经常被忽视一样)图灵完全(比如,参见[150])。在模板机制的设计中,我已经把目标定为完全的一般性和灵活性。一般性是由Erwin Unruh的早期的标准化模板所说明。1994在扩展工作小组会议上,他展示了一个在编译期用于计算质数的程序(使用错误信息作为输出机制)[143],让人感到惊讶,我和其它人认为那是不可思议。模板实例实际上是一个小的编译期函数式编程语言。早在1995年,Todd Veldhuizen展示了通过使用模板怎么定义编译期的if语句和怎么使用这样if语句(和switch-语句)在多种数据结构和算法中进行选择[148]。下面是编译期的if语句还有它的一个简单使用:
template
struct if_ {
typedef X type; // use X if b is true
};
template
struct if_
typedef Y type; // use Y if b is false
};
void f()
{
if_
// ...
}
如果类型Foobar的大小小于40,那么变量xy的类型是Foo,否则是Bar。当模板参数与
Todd Veldhuizen还贡献了表达式模板(expression template)的技术[147],最初作为实现他的高性能数值库Blitz++[149]的一部分。核心思想是通过一个操作符返回一个函数对象,该对象包含最后要求的值的参数和操作,去获得编译期解析和延迟估计。在§ 4.1.4 中operator<产生一个Less_than对象就是一个简单的例子。David Vandevoorde独立发现了该技术。
这些技术和其它利用模板实例化时的计算能力的技术,为给定情形产生完全匹配的源代码的想法提供了技术基础。它可能导致恐怖的含糊和较长的编译时间,但对于艰难的问题,同时也是优雅和高效的解决方法,参见[3,2,31]。基本上,模板实例依赖于重载和特化去提供合理的完整的函数式编译期编程语言。
关于泛型编程与模板元编程之间的差异的定义并没有达成普遍的共识。然而,泛型编程倾向于强调每一个模板参数类型必须有一个可以枚举的一组属性;即是说,它必须能够定义一个概念用于每一个参数(§ 4.1.8 ,§8.3.3)。模板元编程则并不总是那么做,比如,以if_例子来说,模板定义的选择是基于一组有限的类型参数。比如它的大小。因此,两种编程风格不是完全不同。随着对参数的要求越来越多,模板元编程混合到泛型编程中。通常,这两种风格被组合使用。比如,模板元编程能用于在一个程序的通用部分中去选择一个模板定义。
当对模板使用的关注非常强烈地放在组合和其替代方法的择抉上时,这种编程风格有时也称之为“generative programming”[31]。
7.2.3 多范例编程
对于程序员来说,由C++支持的不同的编程风格是很重要的。通常,最优的代码要求使用4种基本“范例”中的多种。比如,我们用C++可以写经典用SIMULA BEGIN[9]实现的“画出在同一个容器中所有图形”的例子:
void draw_all(vector
{
for_each(v.begin(), v.end(), // sequence
mem_fun(&Shape::draw)); // operation
}
这儿,我们使用面向对象编程风格通过Shape类层次获得运行期的多态。我们使用泛型编程用于参数化(标准库)容器vector和参数化(标准库)算法for_each。我们使用普通过程编程风格的两个函数draw_all和mem_fun。最后,调用mem_fun的结果是一个函数对象。一个类不是类层次的一部分并且没有虚函数,则可以被划分进抽象数据类型编程。注意到vector,for_each,begin,end和mem_fun是模板,当使用它时会产生最恰当的定义。
我们能概括出任何序列能一对ForwardIterator定义,而不仅仅是vector,并且使用C++0x的概念可以改进类型检查(§ 8.3.3 ):
template
void draw_all(For first, For last) requires SameType
{
for_each(first, last, mem_fun(&Shape::draw));
}
我认为这是对适当地表现多范编程一个挑战,以便它足够易用,从而为大多主流的程序员所使用。这将涉及找到一个更能描述它的名字。也许它将会从增加的语言支持中获益,但那将是C++1x的任务。
7.3 库、工具包和框架
所以,当我们冲击一个C++语言显然不胜任的领域时,我们能做什么?标准的答案是:创建一个库支持应用程序的概念。C++不是一个应用程序语言;它是一个有许多设施支持设计和实现优雅和高效库的语言。许多关于面向对象编程和泛型编程的谈话都是关于创建和使用库。除了标准库(§4)和TR库的组件(§6.2)外,广泛使用的C++库还包括:
·ACE—分布计算
·Anti-Grain几何学—2D图形
·Borland Builder(GUI builder)
·Blitz++[149]—vector库,“认为自己是一个编译器”
·Boost[16]—基于STL、图形算法、正则表达式匹配、线程、…
·CGAL[23]—计算几何学
·Maya—3D动作
·MacApp和PowerPlant—Apple的基础框架
·MFC—微软窗口基础框架
·Money++—银行
·RogueWave 库 (前STL基础库)
· STAPL[4],POOMA[86]- 并行计算
· Qt [10],FLTK[156],gtkmm[161],wxWidgets[162] -GUI库和创建者
· TAO[95],MICO,omniORB- CORBA ORBs
· VTK[155] - 可视化
在这里,我们使用“工具包”去描述由编程工具支持的一个库。在这种意义上,VTK是一个工具包因为它包含产生在Java和Python中使用的接口工具,Qt和FLTK是工具包因为它们提供GUI创造器。库和工具的组合是对方言和特殊用途语言的一种重要的替代[133,151]。
一个库(工具包、框架)能支持一个巨大的用户社团。这样的用户社团可能比许多程序语言的用户社团还要大,一个库能够完全主宰它们的用户的世界观。比如,2006年,Qt[10]是一个有大约7500个付费客户和150,000个开源版本[141]用户的商业产品。两个挪威程序员,Eirik Chambe-Eng和Haavard Nord在1991-1992年开始,并且在1995发布第一个商业版本,这随后变成了Qt。这是流行的桌面KDE(用于Linux、Solaris和FreeBSD)和著名的商业产品,比如Adobe Photoshop Element,Google Earth和Skype(VOIP)的基础。
对于C++的名声是个不幸,好的库大家看不到;它只是在它的用户后面默默无闻地干活。这常常让人们低估了C++的使用。然而,“当然有视窗基础类(MFC),MacApp和PowerPlan—大多数Mac和微软商用软件是由这些框架所创建的” [85]。
除了这些一般和特定领域的库之外,还有许多用途更特殊的库。它们的功能被限定到一个特定的组织或特定的应用程序。即:他们把使用库作为应用程序设计的哲学:“首先通过库扩展语言,然后用扩展的语言完成应用程序”。由Celera Genomics使用的“string库”(部分大系统称之为“Panther”),成为人类基因序列[70]工作的基础,就是这种方法的一个伟大的例子。“Panther”只是许多在生物学和生物学工程的一般领域中使用的许多库和应用程序中的一个。
7.4 ABI和环境
在更大的范围使用库并不是说没有问题,C++支持源码层的兼容性(但只提供弱链接时刻的兼容保证)。如果满足以下条件,这可以很好地工作:
·你有所有的源代码
·你的代码只用你自己的编译器编译
·你的源代码中的不同部分是兼容的(比如,关于资源使用和错误处理)
·你的代码全都是用C++写的
对于一个大的系统,上面的假设没有一个是站得住脚的。换句话说,链接就是问题的来源。这个问题的根源在于基本的C++的设计决定:使用已有的链接器(D&E §4.5)。
它并不保证符合语言定义的两个C编译单元在经过不同的编译器编译后,能够被正确地链接。然而,对于每一个平台,都达成一个ABI协议(应用程序二进制接口),对于所有的编译器,寄存器的使用、调用惯例和对象布局是一样的,因此C程序员就能正确地链接。C++编译器同样也使用这些函数调用的惯例和简单的结构布局。然而,传统的C++编译器提供商抵制用于类层次的布局、虚函数的调用和标准库元件的链接标准。这导致的结果是一个合法的C++程序可以正常工作的前提是每一部分(包括所有的库)必须被同一个编译器所编译。在同一平台,比如Sun和Itanium(IA64)[60],C++的ABI标准是存在的,但只有“使用单一编译器或通过C函数和structs进行通信”是惟一的一条能生存的规则。
坚持只用一个编译器能保证在同一个平台上的链接兼容性,在提供多平台的可移植性上,它也是一个有价值的工具。通过一致地使用同一编译器,你会得到“bug兼容性”和向所有由提供商所支持的平台移植程序。比如微软平台,微软C++提供这样的一个机会;对于更多的平台,GNU C++是可移植的;对于不同的平台,使用者会惊喜地发现,当他们的本地的编译器使用一个EDG(Edison Design Group)前端时,这会让他们的源代码具有可移植性。当每一个C++编译器都通过一系列升级,部分地增加与标准的一致性,去适应平台的ABI,去改进性能、调试和通过IDE进行集成等,在这些时期内,会让问题变得模糊不清。
与C的链接兼容性产生许多问题,但也产生了一些重大的好处。Sean Parent(Adobe)观察到:“我看到C++成功的一个理由是它与C“足够近”,平台提供者能够提供单一与C++兼容的C接口(比如Win32 API或Mac Carbon API),许多库提供了与C++兼容的接口因为这样的代价是不大—相反,如果提供一组用于Eiffel或Java语言的接口,那将是一个巨大工作,提供语言兼容性的工作量远远超过了保持与C一样的链接模式的工作量。
显然,人们尝试过很多种方法去解决链接器的问题。平台ABI就是解决方法之一。CORBA是一个与平台和语言无关(或几乎无关)的、到目前为止被广泛使用的解决方法。然而,看起来C++和Java才是被CORBA使用的语言。COM是与微软平台无关并且是与语言无关的解决方案(或几乎无关)。Java是基于这样的一种观察而产生的:获得平台与编译器的独立性;JVM通过消除语言无关和完全的特定链接解决了这个问题。微软的CLI(普通语言框架)通过要求所有的语言支持类似于Java的链接、元数据和可执行模型,从而解决了语言无关的问题。基本上所有的这些解决方法都是通过提供一个统一平台而达到提供平台无关性:在一台新机器上或操作系统上使用,你需要移植一个JVM或ORB等。
C++标准没有直接提出平台不兼容的问题。使用C++,平台无关性是通过提供与平台相关的代码(典型的是依赖于条件编译—#ifdef)。这通常是杂乱和ad hoc。但高度的平台无关性是可以实现的,通过把与平台相关的依赖局部化—也许是放在单一的头文件中[16]。在任何情况,实现平台独立的服务是有效的,你需要一种语言,能够利用不同硬件和操作系统环境的独特性。通常这个语言是C++。
Java、C#和许多其它语言依赖于元数据(即,数据的类型是跟语言有关的)和依赖于这些元数据所提供的服务(比如把一组对象传送到另一台计算机)。此外,C++只有一个最低要求。惟一的“元数据”就是RTTI(§ 5.1.2 ),用于提供类的名字和它的基类列表。当讨论RTTI时,一些我们梦寐以求的工具会提供更多系统需要的数据,但这些工具并没有变得公共、通用或标准。
7.5 工具和研究
从1980年代后,C++开发者得到C++世界中存在的许多分析工具、开发工具和开发环境的支持,比如:
·Visual Studio(IDE:微软)
·KDE(桌面环境;自由软件)
·Xcode(IDE;Apple)
·lint++(静态分析工具;Gimpel软件)
·Vtune(多层性能优化;Intel)
·PreFAST(静态源码分析;微软)
·Klocwork(静态代码分析;Klocwork)
·LDRA(测试;LDRA Ltd)
·QA.C++(静态分析;编程研究)
·Purify(内存泄露查找器;IBM Rational)
·Great Circle(废料收集器和内存使用分析器;Geodesic,然后是Symantec)
然而,工具和环境对于C++来说都是其弱项。问题的根本在于分析C++很困难。语法对于任意的N不是LR(N)。那显然是很荒谬的。问题产生是因为C++是直接基于C的(我从Steve Johnsom那里拿来了基于YACC的C句法分析器),C是“已知的”不能由LR(1)语法表达的,直到1985年Tom Pennello发现怎么去实现它。不幸的是,到那时起我已经定义C++以这样的方式工作,Pennello的技术不能应用到C++,因为有太多的代码依赖于非LR语法。句法分析的另一个方面问题是在C预处理器中的宏,它们保证当看到一行代码时,程序员所看到的—经常—显著地与编译器进行句法分析和类型检查时所看到的不同。最后,高级工具的创造者同样也面临着名字查找、模板实例和重载解析的复杂性。
通过组合,这些复杂性挫败了许多去建造用于C++的工具和编程环境的尝试。这个结果就是太少软件开发和源代码分析工具被广泛使用。还有,创造工具的代价通常很高。这导致学术实验比相对容易分析的语言实施要少一些。不幸的是,许多人都带有这么一个态度“如果它不标准,那么存在就没有意义”或者是“如果它需要很多金钱,那么它就没有存在的价值”。这导致了对已存在的工具缺乏了解和充分利用,导致更多的挫折和浪费更多的时间。
间接地,句法分析问题已经让C++在那些依赖于运行时刻信息的领域(比如GUI创造者)变得软弱,由于语言没有要求它,编译器一般不会产生任何形式的元数据(超出RTTI的最小要求;§ 5.1.2 )。我的观点(在原来的RTTI论文和D&E上有阐述)是工具能产生用于特定应用程序或特定领域所需要的类型信息。不幸的是,句法分析问题妨碍了它。工具产生元数据方法已经成功地用于数据库访问系统和GUI,但代价和复杂性让这种方法不能广泛地使用。特别地,由于学生(或教授)没有时间去进行重大的基础建设,使得学术研究再次遭受打击。
对性能的关注也降低了工具的使用范围和数量。象C,C++是被设计用于保证最小的运行时和空间开销。比如,标准库vector默认的就没有范围检查,因为你可以基于不带范围检查的vector创建一个优化的带范围检查的vector,但你不能在一个带范围检查的vector上创建一个不带范围检查的vector(至少不可移植)。然而,许多工具依赖于额外的动作(比如数组的范围检查或确认指针合法)或额外的数据(比如用于描述数据结构布局的元数据 )。C++社团比C社团更加关注于性能,通常这是一件好事,但它对工具创建有一定的限制作用,通过强调最小化,甚至当没有严重性能问题时也是如此。特别地,没有理由C++编译器不能提供好的类型信息用于工具创建者[134]。
最后,C++是自己成功的一个受害者。研究者不得不与公司(有时是正确的)竞争,认为公司有钱去制造研究者想要制造的各种各样的工具。关于性能有一个奇怪的的问题:C++太有效了以致那些真的想获得性能的研究,没能获得成果。这导致许多研究者转战到那些没有效率的语言上去,消除语言中没有效率的东西。消除虚函数的调用就是一个例子:通过对其它面向对象编程语言进行改进,就可以获得大的性能提升。原因是C++的虚函数调用是非常快,一般C++在时间关键的操作中都使用非虚函数调用。另一个例子是废料收集器。这个问题是一般C++程序员不会产生太多的废料和基本操作是很快的。那使得修复一个废料收集器的开销看起来比较没有吸引力,而对于比较没有效率的基础操作和更多废料的语言,则会有大比例的性能提升。再一次,其后果是C++在研究和工具方面较弱。
7.6 C /C++兼容性
C是C++最接近的语言,对C具有高度的兼容性总是C++的设计目标。在早期,兼容性的主要原因是去共享基础组织和保证完全一致(§2.2)。稍后,兼容性被认为很重要是因为在应用领域和程序员群体与C有巨大的交叠。在80、90年代 对C++作了许多小的改变,目的是把C++与ISO C靠得更近[62](C89)。然而,在1995-2004,C同样也在演化。不幸的是,C99[64]比C89对C++的兼容性差一些,并且更难同存。参见[127],对C/C++关系的有详细讨论。下图是C各种版本与C++的关系图:
“古典C”就是大多数人认为的K&R C,但由于由[76]定义的C缺少结构拷贝和枚举。ARM C++是由ARM定义的C++,那是大多C++标准之前的C++的基本。到目前为止基本观测报告是C(C89或C99)和C++(C++98)是兄弟(带类的C为他们共同的祖先),而不是传统的观点,认为C++是C的某种不兼容的方言。这是一个重要事件因为人们一般宣布每一种语言都有单独演化的权力,除了ISO C++,所有人采纳了ISO C所采纳的特征—而不管C的单独演化和C委员会接纳的那些与C++已经提供的相似但不兼容C++的特征的趋势。从长长的列表[127]中挑出几个例子:bool、inline函数和complex数字。
7.7 Java和Sun
我不愿意拿C++跟其它编程语言进行比较。比如,在D&E[121]中“致读者”中,我写到:
几个审稿人都要我做一些C++语言与其它语言的比较。关于这个我已经决定不做了。在此我重申自己长期的且强烈持有的一个观点:语言的比较很少是有意义的,更少是公平的。对于重要语言做一个很好的比较需要付出许多精力,实际上大大起出了大部分人所愿意付出的,超出了他们所具有的在广泛应用领域的经验。为此还需要严格地维持一种超然的不偏不倚的观点和一种平和的理性。我没有时间,而且作为C++的设计者,我的公正将永远不能得到足够的信任…更坏的是,当一个语言比另一个更出名时,会发生这种看法:知名的语言的缺陷被认为是次要的,并且有简单的解决方法可以弥补,然而另一个语言的类似的缺陷,则被认为是基础的。通常,在不那么知名的语言中的解决方法不为人所知且被认为是不能满足要求的,因为那不能在他们熟悉的语言中工作…因此,我不对C++之外的其它语言进行一般和特定的评论。
然而,已经有许多关于C++/Java关系的说法,Java在市场中的存在已经影响到了C++社团。因此,一些评论不可避免,尽管恰当的语言比较不在本文的考虑范围之内,甚至在C++的定义中也没有Java的踪影。
为什么不?从Java早期,C++委员会总是包含那些有许多Java经验的人:使用者、实现者、工具构建者、和JVM实现者。我认为在基础层面上,Java和C++是在传达想法时有很多不一样的东西。特别地:
·C++依赖于直接访问硬件资源,去获取它的许多目标,然而Java依赖于虚拟机,从而让它远离硬件
·C++只带有限的运行时刻的支持,而Java依赖于许多元数据
·C++强调与用其它语言写的代码的互动,并且共享同一系统工具(比如链接器),而Java的目标是通过将Java代码与其它代码隔离,从而达到简化的目标。
C++和Java的“基因”很不一样。Java和C++之间的语法相似总带有欺骗性。作为一个类比,我注意到其它与英语很近的语言,比如法语和德语,很容易采用“结构化元素”,但对于日语和泰语来说,那就要困难得多。
Java在许多公司支持和市场(更多的目标是非程序员)中,进入到编程领域。根据Sun的关键人物(比如Bill Joy),Java是作为C++的改进和简化。“如果还有Bjarne没有设计的东西,那么都必须与C兼容”—仍然很让人惊异—一句经常被听到的语句。Java却不必那么做;比如,在D&E § 9.2.2 ,我概述出用于C++的基本设计准则:一个比C++更好的语言将意味着什么?考虑第一位的决定:
·使用静态类型检查和Simula类似的类
·分离语言与环境间的关系
·C源代码兼容性(“尽可能地近”)
·C链接和布局兼容(“真正局部变量”)
·不依赖于废料收集
我仍认为静态类型检查是好的设计和运行时效率的本质。如果让我重新设计一种新的语言用于今天C++语言所完成的工作,我还是会遵从Simula的类型检查和继承模型,而不是Smalltalk或Lisp的模型。正如我多次所说的那样,“如果我想模仿Smalltalk,我会创建一个更好的仿制品,Smalltalk是周围最好的Smalltalk,如果你想要Smalltalk,使用它”。
我认为今天我能够更清楚地表达它,但本质是一样的;这些标准是定义C++使之成为一个系统编程语言,我不会放弃。对于那段话,今天看起来跟Java更有关系。有内建数据类型和操作符直接映射到硬件的设施,并能从本质上利用所有的机器资源就是隐式地与“C兼容”。
C++并不满足Java的设计准则;它也不想满足。类似的,Java也不满足C++的设计准则。比如,考虑一对语言-技术准则:
·对用户定义类型提供与内建类型一样好的支持
·不给C++以下的底层语言生存空间(除了汇编)
许多不同能被归结为C++为了使自己成为系统编程语言,并在最底层有能力以最低开销处理硬件。Java把自己的目标定为成为一种应用程序语言(对于某些定义的“应用程序”)。
不幸的是,Java支持者和他们的市场机器不是宣扬Java的本质,而是对Java与其它语言(大多指C和C++)做了许多不公的比较。正如2001年晚期,我听到Bill Joy声称(可能是在技术介绍时口头上说的)说“可靠的代码不能用C/C++写,因为它们没有异常”(参见§5,§5.3)。我认为Java是Sun用于对付微软的商业武器,但却误伤了旁观者:C++社团。它同样伤害了许多更小的语言社团;比如Smalltalk、Lisp、Eiffel等。
尽管有许多Java不会取代C++的承诺(“Java会在两年内完全干掉C++”是1996我参加一个图形发布会时反复听到的)。事实上,C++社团在Java第一次出现之时到已经翻了三倍。Java没有真的干掉C++,然而,通过转移需要用于工具、库和技术工作的精力和资金,伤害了C++社团。另一个问题是Java鼓励一个狭隘的“纯面向对象”的编程观点,过分强调运行时的解析和轻视静态类型系统(只有在2005年Java引进了“generics”)。这导致许多C++程序员模仿写了许多不优雅、不安全和效率低下的代码。当这种狭隘的观点影响到教育,并在学生中引起对不熟悉的事物的恐惧时,这个问题就变得特别严重。
正如当我第一次听到有关Java的简单和性能的吹嘘时所预测的那样[136],Java迅速添加新特征—有些是与C++一样的。新的语言总自称是“简单”并对现实世界的应用程序是有用的,然后它们在大小和复杂度上不断增加。不管是Java还是C++都不能摆脱这些效应。显然,Java已经在效率上取得了很大的进步—考虑到它的最开始的缓慢,它当然会取得很大的进步—但当抽象被严重使用时,Java的对象模型抑制了它的性能(§ 4.1.1 ,§4.1.4)。基本上,C++和Java在目标、语言结构和实现模型上的不同比大多数人所认为的要大得多。有一种观点认为Java是另一种受限的Smalltalk版本,在它的运行时刻模型之后是一个类似于C++的语法和静态的类型检查接口。
我的猜测是Java能与C++进行比较的真正力量在于它的二进制兼容, Sun事实上已经通过对Java虚拟机的定义,控制了链接器。这给了Java链接兼容性,那是C++社团所没有的,因为使用基本的系统链接器和C++提供者没有在关键平台上达成一致的协议(§7.4)。
在1990初期,Sun基于Mike Ball和Steve Clamage的Taumetric编译器,开发了一个良好的C++编译器。当Java发布后,Sun大声宣布支持Java方针,其中C++代码被称之为“合法的代码”但由于“污染”需要重写。C++受到一些打击。然而,Sun从不动摇它对C++标准工作的支持,并且Mike、Seve和其它人都做了重大的贡献。从技术上说,人们必须接受技术现实—比如事实上许多Sun用户和许多Sun工程依赖于C++(全部的或者部分地)。特别的,Sun的Java的实现者,HotSpot,本身就是一个C++程序员。
7.8 微软和.Net
微软是当前软件开发中的巨无霸,它与C++有着各种各样的关系。他们在80年代后期第一次尝试一个基于面向对象的C,而不是C++。并且我对它在标准一致性摇摆的两种矛盾的情感有深刻印象。微软通过设置事实上的标准而不是严格遵守的正规的标准而更为人所知。然而,它们在很早之前就制造出一个C++编译器。它的主要设计者是来自以色列公司Glockenspiel的Martin O'Riordan,他是Cfront专家,制造和维护许多移植版本。他曾经制造出一个能把错误信息以浓重的丹麦口音通过语音合成器输出的Cfront,娱乐自己和他的朋友。今天,有许多前Glockenspiel公司的以色列人在微软的C++团队。
不幸的,第一个发布并不支持模板和异常。虽然最后,这些关键特征也被很好地支持了,但那是在多年之后。第一个提供与完整ISO C++特征相近的微软编译器是1998年7月发布的VC++6.0。它的前辈,VC++5.0是在1997年2月发布,已经带了很多关键特征。在那之前,微软管理员使用高度可视化平台,比如conference keynotes,对这些特征(由他们的竞争对手所提供的,比如Borland)进行打击,说它们是“昂贵且无用的”。更坏的,微软的内部工程不能使用模板和异常,因为它们的编译器不支持这些特征。这确立了一个坏的编程实践并造成了长期的损害。
除此之外,微软是C++社团的一个负责人和常驻成员。微软过去、现在都向委员会会议派出其代表—有点迟—但提供了一个非常好的符合标准的C++编译器。
为有效地使用微软把其作为视窗特点的.Net框架,语言不得不支持象Java那样的一组设施。这暗示支持许多语言特征包括许多元数据机制和继承—由可变数组完成(§ 4.1.1 )。特别地,一个语言习惯于产生组件,假定其它语言必须能够产生或接受这些元数据。微软的C++方言支持所有的ISO C++加“对ISO C++的CLI扩展”,口头上指的就是C++/CLI[41],会在未来的C++世界扮演重要角色。有趣的,带C++/CLI扩展的C++是惟一一种提供访问每一个.Net特征的语言。基本上,C++/CLI是对ISO C++的一组巨大扩展,并在一定程度上提供与Windows集成,那使得如果一个程序依赖于C++/CLI特征,那将会在非Microsoft平台变得不可移植,因为它依赖于Windows平台提供的许多C++/CLI的巨大的基础设施。跟以前一样,系统接口可以被封装,并且 必须被封装以保持可移植性。除了ISO C++之外,C++/CLI提供了自己循环结构,重载机制(indexer),“属性”,事件机制,废料收集堆、不同的类对象实例化语义,新形式的引用,新形式的指针,泛型,一组新的标准容器(.Net)等。
在.Net的早期(大约2000之前),微软支持一种称之为“managed C++”[24]的方言。它通常被认为是“漂亮但不中用的”(对微软的某些用户来说),并且好象是一个停止演化的东西—没有任何清晰的迹象表明它将给它的用户带来些什么样的特征。它被更复杂和更小心设计的C++/CLI所取代。
我在标准委员会工作的主要目标之一是就是防止C++分裂成各种方言。显然,对于C++/CLI,我和委员会的努力都失败了。C++/CLI被ECMA[41]标准化,作为C++的一个捆绑。然而,微软的主要产品和他们的主要客户,仍然是使用C++。在可以预见的将来,在Windows平台下,这确保有好的编译器和工具支持C++—ISO C++。那些关注可移植性的人们可以在微软的扩展周围编程去确保平台独立(§7.4)。微软编译器默认会对那些使用C++/CLI扩展进行警告。
C++标准委员会的成员很高兴看到C++/CLI是ECMA的一个标准。然而,尝试将ECMA标准提升到ISO标准引起了洪水般的不满。一些国家标准机关—比如UK的C++小组—公开表达严重关注[142]。这使得微软的人们去更好地文档化他们的基本设计原理[140]和更小心地分清ISO C++和微软自己的C++/CLI。
7.9 方言
显然,不是每个人都认为通过漫长的ISO标准化进程去让他们的想法融入到主流是一个好主意。类似的,一些人认为兼容性被高估了,甚至是一个坏主意。在以上两种情况中,人们感到他们能够加快这个进程或者觉得通过简单定义和实现他们自己的方言更好。一些希望他们的方言最终会变成主流的一部分;一些认为生活在主流之外对于他们的目的来说也是好的;还有少数的目标就是制造一种短期的语言用于实验用途。
对于我来说,那已经有太多的C++方言了(许多打)。这还没有坏事—尽管我不赞同方言因为他们在分化使用者社团[127,133]—但也反映了它们在限定的社团之外的影响非常有限。到现在为止,没有一个主要语言特征是从方言中引入到C++主流的。然而,C++0x将会有所改变,引入有点象C++/CLI的恰当的域enum[138],有点象C++/CLI的for语句[84]和关键字nullptr[139](很奇怪,那是我对C++/CLI的建议)。
并发看起来是个极度受欢迎的语言扩展领域。下面是一些C++方言,通过语言特征从某种程度上支持并发:
·Concurrent C++[46]
·Micro C++[18]
·ABC++[83]
·POOMA[86]
·C++//[21]
基本上,计算机科学世界中的每一次潮流和衰落都会产生许多C++方言,比如Aspect C++[100],R++[82],Compositional C++[25],Objective C++[90],Open C++[26]和许多其它的。这些方言的主要问题是它们的个数。方言总是分裂一个子用户社团,使之没有足够大的用户社团,从而不能提供一个可以支撑的底层建筑[133]。
当提供商往语言中添加某些(典型的很少的)“巧妙的特征”到它们的编译器去帮助他们的用户去实现某些特定任务(比如,OS访问和优化)时,另一种方言就出现了。这些都成为移植的障碍,尽管它们受到一些使用者的喜爱。它们同时也受到一些管理者和平台狂热者的赏识,作为一种占据(lock-in)机制。几乎每个编译器提供者都这么干过;比如Borland(Delphi风格的属性),GNU(变量和类型属性)和微软的(C++/CLI§7.8)。当这些方言特征在头文件中出现时,它们是肮脏的。引爆提供商之间的为了能够处理他们竞争者的代码的战争。同时他们还提供非常重要的编译器、库和建设工具。
当一种方言变成一种完全独立的语言是不明显的,许多语言从C++那里借用了许多东西(有些承认,有些不承认)而没有任何兼容性的目的。一个近期的例子是“D”语言(最近的有着最流行的名字的语言):“D是在1999年12月由Walter Bright构想,作为对C和C++的重新设计”[17]。
甚至Java的出发点(非常短暂)就是作为C++的一种方言[164],但它的设计者马上决定维护与C++的兼容性限制了他们的需求。因为他们的目标包括对Java代码的100%的可移植性,限制代码风格为面向对象编程风格。在这个方面他们的确是正确的,达到任何程度的有用的兼容性是非常难的,正如C/C++的经验所表明的那样。