山雨欲来风满楼
——标准C++及C++/CLI发展综述
声明:此文发表于2004年11期程序员杂志上,此版本比杂志版本有所添加。 并禁止任何转载!
刘未鹏(pongba) /文
when programmers are faced with a choice between productivity and control, technologies that make them more productive tend to win out over time.
——Don Box
自从Java诞生的一开始,语言以及开发平台的竞争似乎才真正开始无休无止起来,以前的那些单纯的时光似乎离我们越来越遥远。各种新名词仿佛是在一夜之间全部涌现了出来。几乎在每个公司的招聘广告上总能看到"Java,J2EE,C#,.NET..."等字样。我们的时间似乎越来越不够,在各种新概念面前,唯有埋头不断啃书,不断学习,我们身不由己,总担心一有懈怠就会立即落在潮流的后面...
诚然,这些新事物总是有存在的理由,对于做高层应用开发的人们,Java以及J2EE,C#以及.NET似乎(也的确)给了人们一个很好的承诺,并且确实实现了他们的承诺。在Web的世界里,这些语言和平台一出身就体现出了无比的优势,因为其架构本身就是为这个世界构建的。
然而,鱼和熊掌总是不可兼得,语言的简洁性固然简化了我们的学习过程,然而却削弱了代码的表达力。托管执行的引入固然消除了平台相关性,却损害了效率(至少在目前,这仍然很大程度上限制了托管语言的发挥空间)。GC固然简化了内存管理,固然是大势所趋,使得程序员可以从内存管理的"雷区"脱身而出,然而更一般的却是资源管理的问题,程序员仍然没有少费多少心思,并且GC的世界也并不单纯,程序员有了依托就可以大胆的去new,用完了就丢,某种程度上GC鼓励了垃圾的产生,更为重要的是,GC本身的速度仍然是个问题,虽然硬件和GC算法总在不断的改进,但是在某些性能要求苛刻以及内存有限的环境中(如嵌入式系统),GC仍然还是个谨慎的选择。
然而,光芒毕竟还是掩盖了尘埃,在厂商们的大力宣传下,我们似乎也变得勇往直前,却很少有人注意,在喧嚣的背后,有一门语言仍然在冷静的发展着,这就是C++。
是的,对于C++的发展,我只能用冷静来形容,因为C++社群中集中了太多的智慧,太多来自各个领域的专家提供了他们的经验和建议。对于这样一门"公共"的语言,每一步发展必然是经过千锤百炼的,并且经受得住时间考验的。
标准C++
从某种意义上说,C++仍然是一门很年轻的语言,从98年C++的第一次标准化到现在,虽然语言的核心并没有变化,但是库的发展却未曾有一日停止过:STL,Boost,ACE,LOKI,MTL,Blitz++...这些熟悉的名字正在渐渐深入人心,这些库所带来的思想和效益正逐渐深入工业界,这些库保持着C++长盛不衰的活力。
而现在,C++社区又在酝酿一次新的变革,这就是所谓的C++ 0x标准,也就是C++的下一代标准,其中“0x”可能是07~09,虽然看起来C++0x离我们还有好几年,但是从98年到04年,关于C++标准(语言核心及库)的勘误以及提议已经达到500左右[1],其中有很大一部分思想已经成熟,只是需要反复经过时间以及实践的考验而已。当然,我们最为关心的还是,这次变革会给我们带来多少惊喜,简洁的答案是“会有极大的惊喜”。冗长的答案就在下文——
Bjarne Stroustrup的思想——灯塔
这位C++创始者仍然活跃在C++标准化委员会中,毫无疑问,他仍然具有举足轻重的发言权。对于C++ 0x的方向,Bjarne Stroustrup的总体思想是:对语言核心的改动应该是谨小慎微的,而对库的发展则应该是大胆而激进的[2]。看起来非常简单平淡的两句话,却道出了真谛——保持语言核心的简洁紧凑和一致性,仅作必要的小范围改动,可以避免增加语言的复杂度,并将对现存代码的兼容性提到最高。从另一个方面,库的发展则大可不用顾忌这些,C++社区期待一些优秀的,可以提高生产率的库已经很久了。
语言核心的发展——精雕细琢
总的来说,C++ 0x对语言核心的调整是极其理智和谨慎的——避免大的语言扩充,进一步改善对泛型编程的支持,改善对嵌入式编程的支持。然后就是一些小范围的技术勘误和调整,改善语言的一致性等。
如果非要找出一个关于C++语言的关键性改动(它的确存在),就是加入了一个右值引用“&&”和Move语义的加入,虽然看起来这是个很小的改动,但是却对语言的效率产生了非常巨大的影响,因为它完美的解决了临时对象的效率问题。
简单的来说,"Move"语义可以允许一个对象被"Move(搬移)"到另一处(而并非"拷贝"--这是目前的做法),从语义上说,"搬移"意味着将对象整个的搬到目标地点,原来的对象从语义上将不复存在。对于动态分配内存的对象来说,"搬移"意味着将动态分配的内存直接搬到新家(目标对象)里面,而源对象将不复拥有原来的动态内存。说得细节一点,这就相当于一次指针赋值,只要把源对象里的动态内存直接交给目标对象就行了,一次所有权的转交,源对象不再拥有原来的动态内存。Move语义完美的解决了返回值的效率问题(这被认为是C++中影响效率的主要因素),所以具有非常重要的意义。在目前,大多数情况下,即使有返回值优化,仍然会存在不可避免的对象冗余拷贝,函数返回的值至少要被一次拷贝(到目标对象),然后函数栈内的对象被析构掉,这其实是一种浪费,反正栈内的对象总要析构的,为什么不把其中动态分配的资源直接Move到目标对象呢?但是在目前的C++里,很难在拷贝构造函数中知道源对象是否是"可搬移的"--万一源对象并非函数返回的值,而是一个普通的"具名"对象怎么办?你可不能将别人的动态内存"偷"过来。但是,考虑到函数返回值总是右值,而"具名"对象总是左值,所以只要有一种机制,能够区分出左值右值,就能够决定是否从源对象中"偷"内存了。这就是引入右值引用"&&"的原因之一。只要拷贝构造函数接受的是右值,就放心的搬移吧--它必定是个临时对象(当然,也有可能是用户显式要求搬移)--这种搬移必定比“先将动态内存原版拷贝一份过来,然后任由目标对象析构掉原先的那份内存”要快得多。考虑一下目前的std::vector在重新配置内存时候的效率吧--先要将容器里原本存在的对象全都"拷贝"(注意,拷贝可能是相当昂贵的操作)到新配置的空间里面,然后再将原来的对象一一析构。为什么不将它们直接"搬移"(而并非"拷贝"过来)呢?"搬移"是极具效率的动作。没有额外的动态分配(因为不是拷贝,不用照顾源对象的状态,直接吧源对象拥有的动态内存以及资源"偷"过来就可以了,这几乎就是一次指针赋值)。此外,"Move"语义还完美的解决了函数返回值的效率问题。
另外,右值引用(&&)也使得"perfect forwarding"成为可能。其含义是"将一组参数完整保留原来的左右值属性以及CV修饰转发给目标函数的能力"。你可以发现,在现在的语言机制下,这种转发是难以完美的,主要原因是无法判断左右值。右值引用使得函数可以判断参数到底是左值还是右值,从而实现完美转发。
关于右值引用的更多信息可以参见关于C++语言核心的两大提议[3]。
另外,C++ 0x还将很大程度上改善对GP的支持,如decltype,auto,varadic template等(详见C++标准的主页)。
总的来说,所有的语言核心的演化,都必须遵从零开销(zero overhead)原则,即用户不必为他们用不着的东西付出代价(特别是效率的代价),这正是C++在系统级开发领域能够保持长久活力的关键。无论如何,系统级开发是C++的立场,千变万变,立场不变。
库的发展——风起云涌
在C++库的发展的历史中,最为瞩目的当属STL,由于GP思想在其中的完美运用,STL兼得了“鱼和熊掌”——效率和优雅。看来大家都看到了GP的强大表达力,特别是在库的构建方面,所以像Boost,ACE,LOKI,Blitz++,MTL...等库都不约而同的使用了GP,并取得了巨大的成功。可以预见GP仍然会在库的构建方面发挥巨大的能力。
发展最为迅疾的库是Boost[4],因为Boost库是“准”C++标准库,所以其中的很多设施被提议加入下一代C++标准库,如正则表达式库,智能指针库,线程库,泛型函数指针库等,这些都为通用编程提供了极大的支持,另一个重要方面是,这些库的标准化意味着使用它们的代码将是完全可移植的。
另外,被程序员们长久以来所埋怨的serialize能力也在Boost库中得到了解决——boost::serialize将被加入boost库,为跨平台的serialize提供良好的支持,唯一的缺点就是我们等得太久了:-)
之所以C++这两年来“看起来”风头减弱,主要是大家的注意力都被吸引到了J2EE和.NET所擅长的Web开发以及企业应用开发领域里去的缘故,而C++在这方面又如何呢?CORBA太庞大,ACE还不够“傻瓜”,ICE据说是以轻量级为目的,但愿如此。但最重要的还是,C++本身对分布式的支持还不够,所以往往要借助其它工具,比如IDL以及IDL编译器,IDL某种程度上就是类型信息(如支持哪些接口)的载体,它在客户端和服务器端传递(承载)类型信息,但在Java或.NET中,IDL不是必要的,因为类型信息由元数据(metadata)来承载,所以在.NET Remote和Java RMI架构下对象的远程沟通很方便,从而可以很方便的构建一个分布式计算的环境。
而在C++中,这样的日子可能也不会太遥远了,Bjarne Stroustrup正致力于为C++加入分布式计算的能力[5],一旦这成为现实,程序员将可以通过极为简单的几行代码就可以和远在Web另一端的对象交流,远程调用就如同本地调用一样简单直观...这些特性可能会在C++ 0x中和大家见面——当然,以库的形式。我们有理由相信,C++在分布式计算以及Web开发领域也将有一个美好的未来。
同样,为程序员熟知的GUI编程也曾是C++的“软肋”之一。但是这也即将成为过去。一个泛型的GUI库正在悄然兴起。以前的C++ GUI库如WxWindows以及MFC由于只使用了C++的普通特性,所以表达力有限。而这个兴起的库名为Win32 GUI Generics[6],GP与多继承的完美结合,结果是简洁而优雅的GUI框架。
可以看出来,虽然C++的主要阵地是系统级开发,但是无疑在其它更高层的领域也有不可估量的潜力,我们拭目以待。
C++/CLI——微软的又一张王牌
作为微软的“首选”语言,C++在微软心目中占有着异常重要的地位,标准C++的发展固然是独立于平台的,但是为了让开发者们能够在windows下更具效率的工作,标准C++仍然还不够“便利”,微软希望windows下的C++开发者既能够做高效的底层系统级开发,又能够方便的做高层的应用开发。怎么做?.NET?当然是.NET!.NET是微软的王牌,下一代windows将完全建立在.NET的基石上,目前的windows XP的系统API仍然是传统的C接口的WIN32API,而下一代windows(longhorn)将把.NET本身作为系统API。然而,微软最钟爱的语言——C++——和.NET相处得又如何呢?如果把时间往前推半年,答案只能是——差强人意,Managed C++ Extension只不过是一次失败的尝试,将标准C++通过一些丑陋的双下划线为前缀的关键字强制“托管”起来已经被时间证实不是个好主意。其导致语义的模糊以及语言表达力的退化是MC++消亡的主要原因。
为了解决这些问题,并且稳固C++在windows(.NET)平台上的地位,微软将Stan Lippman和Herb Sutter两位大牛人请了过来,重新设计C++在.NET下的表现形式,标准C++由于本质上是编译型语言,所以显然是不够的,然而又不能“从头发明轮子”,怎么办呢?微软选择了最明智的办法——中庸之道——在完全支持标准C++的前提下,引入一些新的语法和语义,从而对.NET环境提供第一流(first class)的支持,这就是C++/CLI。之所以有这个称呼,是因为它从某种意义上说并非一门新的语言,标准C++仍然在其中完整保留着,C++/CLI只不过建立了C++与CLI之间的联系而已,虽然说“...而已”,但是其意义是非常巨大的,C++程序员从此可以以一种非常“C++”的方式去利用.NET中丰富的类,同时仍然可以利用标准C++的强大表达力,可谓左右逢源。C++/CLI的能力可以用下图来描述:
图一. C++/CLI的能力
语法
C++/CLI除了和标准C++相同的部分之外,还增加了一些新的语法元素,以便支持托管环境下的编程,其中最关键的就是加入了托管环境下的“指针”(称为Handle)和引用以及用于在托管堆上创建对象的gcnew,它们的声明形式分别为:
int^ handle = gcnew int(0); //托管的指针“^”,称为Handle
int% managed_ref = *handle; //托管的引用“%”
值得注意的是,对handle解引用也使用“*”,这是为了在模板函数中可以以一致的方式来解引用。
这两个语法形式的加入,确保了在C++/CLI中你可以利用.NET中所有的托管类,并且把托管的世界和native世界从语法上区别开来。
总的来说,C++/CLI的语法完全遵循标准C++的风格(毕竟Herb Sutter是标准C++委员会的成员嘛:-)),这种一致性可以极大的平滑程序员的学习曲线,并使得C++/CLI中的程序风格和标准C++非常接近。另外的语法元素你可以参考微软官方网站上提供下载的C++/CLI候选标准1.5。
语义
加入了托管语义的C++/CLI呈现出一种混合(Mixed)的语义(但并不混淆),你既可以高效的进行Native编程,也可以在必要的时候为了方便而使用.NET中现成的类,两者互补且可以相互沟通,如果想把托管的对象传给Native接口,只要先把该对象“定”在托管堆上(通过pin_ptr<>,不然GC在压缩堆的时候会移动对象,从而导致指向它的native指针统统失效),然后传递指向该对象的指针给Native接口就行了,反之,在托管环境里访问非托管对象则一般不用任何辅助动作。这种混合式的编程环境提供了极大的自由度,也完全兼容了现存的C++代码。
图二. C++/CLI编程环境示意图
STL.NET——又一个诱人的条件
STL.NET是微软吸引C++开发者往.NET环境下迁移的又一个诱人的条件——用标准C++进行开发的程序员对于使用STL已经积累了大量的经验,当然最关心的是.NET托管环境下有没有一个可以和STL相媲美的“容器——算法”库,微软的回答是“当然有”——这就是STL.NET[7]。STL.NET中的容器针对的是托管环境,也就是说,C++/CLI的开发者可以以熟悉而优雅的方式在托管环境中使用(甚至扩展)容器类和算法,当然,原来的“Native”STL依然“健在”,程序员仍然可以在效率要求较高的地方使用原来的STL,一切都依从他们以往的经验——看来微软对C++真是费足了心思。
关于C++——微软的回答
对于在.NET平台上如何选择开发语言,微软的答案是:
Choosing a Language
Q: Which .NET language should you use?
Microsoft’s answer, in our Whidbey release:
• C++ is the recommended path to .NET and Longhorn.
• If you have an existing C++ code base:
Keep using C++.
• If you want to make frequent use of native code/libs:
Use C++. It’s far simpler (seamless) and faster.
• If you want to write brand-new pure-.NET apps that
rarely or never interop with native code:
Use whatever language you’re already comfortable with...
由此可见C++在.NET平台上的重要性,这种重要性反过来也会对标准C++的发展起到积极的作用——毕竟,学习C++/CLI首先意味着学习标准C++ :-),因为.NET平台的广泛性,可以预见会有更多开发者转向C++社群,这是一个良性循环。
C++&C++/CLI的未来——坐看云起时
Bjarne Stroustrup在一次接受采访的过程中曾说:“C++拥有极其美好的未来”。他的自信当然不是盲目的。就目前标准C++的发展趋势来看,C++正在越强大,以前主要在系统级开发领域挥洒的C++正在逐渐进入越来越多的领域,例如,对分布式计算,Web开发以及GUI编程的优秀支持。而作为标准C++的一个超集的C++/CLI则会为托管环境下的C++编程提供优秀的支持。在标准C++发展过程中,最为难能可贵的是,标准C++始终坚持零开销(zero overhead)原则,给系统级开发的程序员一个效率的承诺。
随着C++ 0x 标准愈来愈进的脚步声,C++正在坚定的朝着更辉煌的目标迈进。
C++/CLI是微软给.NET程序员的一个礼物,也是给标准C++的一个礼物。按照Bjarne Stroustrup的思想,标准C++的发展会始终维持编译型语言的实质,而想用C++做更多事情的厂家可以自行对其进行托管环境下的扩展。C++/CLI的出现表明了这种扩展的可行性和成功性。可以预见,完全继承了标准C++中强大的表达力的C++/CLI在.NET平台上将会有最好的表现。同时,标准C++的发展也意味着C++/CLI的发展,C++/CLI享有标准C++发展中的一切成果!
故事才刚刚开始...
[1] www.open-std.orgjtc1sc22wg21docspaperspapers.html。
[2] Bjarne Stroustrup的主页上有一篇文章描述C++0x的发展方向。
[3] Move Proposal www.open-std.orgjtc1sc22wg21docspapers2002n1377.htm和Forwarding Problem www.open-std.orgjtc1sc22wg21docspapers2002n1385.htm。
[4] 关于Boost的发展,没有什么能比它的官方网站更有说服力 http://boost.sf.net。
[5] 参见Bjarne Stroustrup的个人主页www.research.att.com/~bs/。
[6] www.torjor.com或http://sf.net/projects/win32gui/。
[7] 关于STL.NET,Stan Lippman有一篇精彩的阐述——“STL.NET Primer”,见MSDN