C++0x展望[语言核心进化]

声明:本文大部分内容摘自发表在2005年11月期《程序员》杂志上的<C++0x前瞻>一文。为尊重版权,谢绝转载。


刘未鹏 /文

C++标准委员会案头的C++0x提案[1]的与日俱增,tr1[2]的尘埃落定,tr2[3]的浮出水面,C++0x的脚步声似乎是离我们越来越近了,近到我们似乎能够嗅到她的气息,Bjarne StroustrupCUJ上发表的一篇文章『The Design of C++0x[4]高屋建瓴地指出了C++0x的精神,指出了哪些东西可以进入标准,哪些东西应该慎重考虑,哪些东西是迫切需要的。尽管如此,改革还需一步一步来,C++0x中最关键的改革,即语言改革,必须慎之又慎,所有待加入语言的特性都必须经过认真的商榷,斟酌其必要性(有无效果等同的替代方案)、跟其他语言特性之间的关系、对现有代码的影响等问题,如果有必要的话,还须对其进行初步实现、使用,并获取相关的经验,最后才能确定是否加入标准。相比之下,标准库的进化则要开放得多,artima开发社区上发布了一个C++0x Wish List[5]C++开发者可以把自己希望的特性添加进去,由C++标准委员会来仲裁取舍,实际上Bjarne Stroustrup希望C++0x的标准库能够成为一个平台(platform),而不仅仅是库。

C++标准委员会的主页上已经有了大量关于语言改革的提案,几乎涵盖了一门现代的general-purpose语言所应该具备的所有特性提案,当然,除了C++已经做得很好的那些方面之外。然而,C++0x不可能把所有这些东西都放到语言当中去,那样只会让这门语言变成一个拼凑起来的庞然大物,并同时更增加语言的复杂性和学习曲线的陡峭程度;另一方面,虽说C++提倡保持语言内核的紧凑高效,尽量使用库的形式来实现一些特性,而不是总诉诸内建的语言特性,然而有些特性倘若没有语言级别的支持是很难达到它应有的效果的。譬如说Move Semantic如果没有语言级别支持,就得费牛劲才能达到还算凑合的效果,而一旦有了语言级别的支持,即只需对标准作一点小小的改动,就立即达到完美的效果,甚至还能完美解决其他一些问题。更进一步,譬如说像reflection这种特性,倘若没有语言标准结合编译器支持,要想单凭一个库来实现几乎是不可能的,即便跌跌撞撞地实现出来了,其易用性也必定相当差。

本文重点考察到目前为止有关C++0x的语言进化的提案当中的比较突出的一些提案,从中我们可以管窥到C++0x语言核心进化的大致方向以及一些既定的东西。

2002年九月,C++标准委员会的语言核心工作组收到一份提案,“A Proposal to Add Move Semantics Support to the C++ Language”,这份提案由Peter DimovDavid Abrahams以及Howard E. Hinnant发起,Peter DimovBoost.Bind等库的作者,David AbrahamsBoost库的发起者、C++标准委员会的成员、Boost.MPL等库的作者。Howard E. Hinnant也既是Boost库里面的若干库的参与者又是C++标准委员会的成员,这份提案针对C++中长久以来被称为是效率的最大敌人——临时对象——提出了详尽的应对措施。其核心理念在于:之所以临时对象成为C++效率的最大敌人,是因为创建一个具有non-trivial构造函数的临时对象需要耗去一定的时钟周期和系统资源,而这些动作的全部意义只是创建了一个转瞬即逝的对象,当被(深)拷贝到某个具名对象之后,这个临时对象就会被析构掉,因而它所占有的资源和分配它们所消耗的时钟周期的全部意义就仅仅在于在栈上临时保持某些东西的一份副本而已。然而实际上,一个有趣的观察认识到,既然临时对象是没有名字的,那么除了从它进行拷贝的那个对象之外,再没有其他地方能够用到它,这就是说,在进行拷贝的时候,我们可以将临时对象所占有的资源过来,而不用自己去重分配一份,反正被的那个临时对象立即会被析构掉,因此这个的动作是安全的。这种资源的能力在另一类场合下也是有用的,即你知道某个具名对象在这次被拷贝之后就立即被丢弃了,举个例子,当std::vector重新分配它的内部缓冲区时,它会新分配一块row memory,然后将老内存当中的那些对象一一拷贝到新内存当中去,实际上这种拷贝可能是相当低效的,假设vector中存放的是std::string,后者自己会维护一块内部字符缓冲区,这样一来每拷贝一个string就会进行一次分配,而旧元素中的那些现成的资源则会在析构时被丢弃掉,非常可惜,为何不直接把原来那些string对象的内部缓冲区过来呢?反正它们也用不着了,换句话说,这种动作不再是拷贝,而是搬移(Move),后者更为形象,甚至更理所当然。这是完全可行的,然而,要想进行合法的“偷窃”,我们就必须能够区分一个对象是否为临时对象,只有临时对象或者主动请求被搬移的对象的资源才可以被“转让”,通常情况下这也就是去判断它是否为右值。然而在目前的C++标准下,我们没有直接的办法来判断某个对象是否为右值,Andreu Alexandrescu曾在C++ Users Journal 2003年二月刊上发表了一篇文章“Move Constructor”[6],引入了一个精巧的框架(mojo)来解决这一问题,并在04年六月刊上又发表了一篇相关文章,将move语义实际引入了vector当中,创造了一个被称为yasli::vector的高效vector类。然而,虽说mojo可以说是从原则上完美解决了这个问题,但由于其实现依赖于一系列的trick,因此使用起来有一定的难度,初学者很难将其运用自如。因此要想真正将这一能力广泛运用到实践当中,仍然需要语言内建的支持,因而该提案引入一个所谓的右值引用,将获知对象左右值性质的能力暴露给程序员,从而完美地解决了这个长久以来的效率问题。

无独有偶,随后不久,Peter Dimov又发表了一项提案“The Forwarding Problem” [7],这项提案实际上触及了泛型编程中的一个相当重要的问题——实参转发:由于负责转发的模板函数通常并不知道实参的真正类型,而它们同时又要保证在转发的过程中不改变实参的左右值属性跟cv-qualifier,这就是要求我们有这样一种表述参数类型的语法形式:如果实参是左值,其类型就会被推导为引用,如果实参是右值,其类型又必须带有“右值”语义(注意,目前的模板参数推导机制已经能够正确推导出实参的cv属性)。实际上,解决方案是对右值引用的模板参数推导机制作一点小小的修改,就完美地解决了这个问题。因而右值属性这一语言特性的加入可以说是一石二鸟。

我们可以看出,上面这两项语言进化其实都是依赖于一个重要的能力,即判断一个对象是左值还是右值的能力,由于一些历史性的原因,当时的语言规则在这个方面只有一条,即不能将non-const引用绑定到右值,而实际上mojo正是利用这个语言的缺口来实现左右值的判断的,除此之外即将加入Boost下一版的Boost.FOREACH库也使用了另一个匪夷所思的技巧来探测对象的左右值。这正表明了,由于语言规则本身并不完备,所以mojo(和Boost.FOREACH)才需要借助于一系列的辅助措施以及艰深晦涩的trick。由此可见,一个完备的语言规则系统对于语言本身的表达能力仍是相当重要的。C++语言进化的隐含原则之一就是让语言对初学者变得更易于学习,如今的C++语言中trick太多,其中很多都是利用的一些非常细微的语言特性,所以语言进化的目标之一就是给这些trick以更优雅直观的替代方案。

C++泛型编程的目标除了泛化复用代码之外,还有非常重要的一点就是效率,后者从本质上来说就是将决策提前到编译期。而要将决策提前到编译期,就意味着要让开发者尽可能地获取编译期所能知道的信息,换句话说,C++应该从语言特性的层面上提供给开发者用于获取各种有用的编译期信息的能力。右值引用就是一个绝佳的例子,原先C++中只有左值引用,导致没法判断一个对象是否为右值,至多能够判断出一个对象是“右值或const对象”(借助于“右值无法绑定到non-const引用”这一语言规则),进而导致一些重大的优化无法进行。因而如果罗列一下至今的C++0x语言核心提案中的“十大热门”的话,右值引用肯定位居三甲。

GPC++的灵魂之一,虽说GP近几年一直处于不断的发展和成熟当中,同时也不乏一些激动人心的新技术的出现,甚至有了相当完善的元编程库Boost.MPL[8],但C++语言对GP本身的支持仍然还有诸多不尽人意之处,这些不尽人意的方面有的属于枝节问题,譬如说template aliases[9]extended friend declarations[10]、模板函数的偏序规则的一些不完善之处[11]等等,这些在C++0x里面肯定会得到采纳或者精化。而另外一些则是触及到了GP的根基的问题,譬如concept[12]auto&decltype[13]variadic templates[14]这些特性。

C++ GP其实就是在强类型系统的坚实根基上架空起一层形式上(近乎)无类型的系统,这一层系统是由模板相关的语言特性来支持的,在这个系统当中,你通常看到的是TUtypename,而不是intdoublelong,所有对象的类型都处于一种存在但不被关心的状态,直到模板被实例化,类型被落实,这时强类型系统才会运作起来,进行最后的把关——强类型检查,某种程度上,C++ GP有点像戴着枷锁的舞蹈,强类型系统就好比一把枷锁,而其上的GP设计空间则进行着近乎无类型的舞蹈。这里的问题在于,在目前的C++标准下,模板的绝大多数类型检查都是在其实例化的时候被进行的,那些能够在编译器阅读模板定义时就得到类型检查的部分只是一些non-dependent names[C++98:14.6.3],这在一个模板定义当中通常只占极少一部分。然而,如果编译器在看到模板定义的时候就能够对它进行全面的类型检查的话,就能够有力地提高GP中的早期纠错。STL中引入了concept的概念,并通过一些trick实现了concept的统一表示,然而其作用说到底也只能是在模板实例化时对模板参数进行检查而已(从而提供更为友好的编译错误信息),最关键的是,即便某个函数模板的类型参数应当符合某个concept,编译器也无法根据这个concept来对该函数模板的定义体进行早期的强类型纠错,这是因为concept在当前的语言当中并非一类公民,我们无法在语言层面直接表示一个concept,换句话说编译器并不知道concept的存在。从另一个方面来说,concept其实就是契约式设计(DbC)的编译期版本,我们通常看到的DbC是针对运行期值的,而concept则针对类型,一个concept其实就是在模板定义跟实例化该模板的模板实参之间架上了一层契约(contract),双方都应当遵从或假设这些模板参数是符合特定的concept的,对于使用方来说,其提供的模板实参自然会在模板实例化时得到检验,而对于编译器来说,则可以根据对模板参数的先期假定(concept)来对模板定义体进行早期的类型纠错,同时由于编译器能够知道并理解concept,因而如果出现类型错误的话必然能够生成友好得多的错误信息,从而解决C++98模板的一个令人头疼的大难题。此外,一个first classconcept语言特性还能够极大程度上削减模板的文档工作,因为concept自身就是最好最精确的文档。不过,C++98并未提供这种能力,我们无法在模板定义的时刻告诉编译器哪些模板参数被假定为符合哪些concept。鉴于此,Bjarne Stroustrup一直致力于为C++0x提供一个first class的语言特性,用于表达concept[15]这一概念。目前这个语言特性正在被不断精化,concept的两种可能的表达方式也正在被权衡着利弊,虽然一切尚处于动荡之中,然而可以肯定的是,它肯定会被加入到C++0x当中去,从而为GP整体的易用性带来极大的改善。

另外,一个已经获得C++委员会投票通过的语言特性是autoauto提供一种基于初始化表达式的类型来推导被初始化变量类型的能力。这个特性相当简单直观,然而很重要,因为它进一步从形式上将C++GP往无类型的境界推进了一大步:C++模板编程当中经常会出现一些比较复杂的类型(尤其是当涉及到Boost.Lambda这类表达式模板库的时候),即便在模板定义中也常常如此,但大多数时候我们并不关心某个变量的类型是什么,只是关心它有哪些操作或语义,而C++的强类型系统却迫使我们在声明一个变量的时候给出其类型,这就给代码中塞入了不优雅的成分,为了解决这个问题,提案中引入了一个新的关键字:auto,其作用可以说成是一个占位符——既然我们不关心变量的类型而又必须以某种形式来声明其类型,那么就用一个“auto”来占据那个类型的位置吧,这个特性在丝毫不影响强类型系统的同时极大地简化和优雅了模板代码,因而以压倒性的优势获得投票通过。

另一个能够极大地提升C++模板机制的泛化表达能力的特性是variadic templates。当前的C++标准并不能表达“可变数目的模板参数列表”这一概念,因而像tr1::tupletr1::bind乃至tr1::function(这三个原先是boost中的设施目前已经进入tr1)这些模板就必须借助于一些非常繁琐的技巧来实现可变参数列表的能力,而且这种模拟的可变参数列表也只能算是个半成品,譬如tr1::tuple实际上接受的模板参数个数是有限的,而tr1::bind也最多也只能绑定有限多个参数,Boost.Bind里面目前实现到了绑定九个参数就已经引入了大量的重载,不但增加了重复代码量,而且很大程度上增加了编译时间。而Boost.Function为了支持不同数目参数的函数,则是通过一些非常晦涩复杂的宏技巧来实现的,非但降低了代码的可读性,同时也增加了代码量和编译时间。因而一个语言层面支持的可变模板参数列表似乎是必然的要求,有了这一特性,这三个类模板的实现以及其他涉及可变模板参数列表的模板代码都可以得到极大的简化,我们可以从提案当中看到这一点。目前variadic template这一提案仍然处在不断精化当中,但我们有理由相信它一定会出现在C++0x当中。

显而易见,有关语言核心进化的绝大部分重大提案都是与模板相关的,因为模板是C++区别于其它语言的重要标志之一,也是在C++当中获得重大成功的特性,其带来的影响可以说是革命性的,我们有理由相信C++0x中的模板会带来再一次的革命。

此外,有望进入C++0x的语言核心进化提案还有诸如”nullptr””delegate constructor””strong typed enum””opaque typedef””explicit conversion operator”等,这些提案的内容顾名思义。与上面提到的提案相比,这些提案只能算是对当前语言核心中的一些小修补或精化,改善语言的表达力,使C++0x的编码变得更为优雅。除此之外我们还可以在C++标准委员会的网站上看到一份”C++ Standard Core Language Issue List”,里面是众多关于当前的语言核心的一些缺陷或模糊之处以及解决方案,这些都会成为C++0x对语言核心进行精化的依据。从98年第一代标准颁布至今,C++标准委员会获得了许多宝贵的经验,C++0x这个第二代标准必然会比C++98成熟得多。

[1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/

[2] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1745.pdf

[3] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1810.html

[4] http://www.research.att.com/~bs/rules.pdf

[5] http://www.artima.com/cppsource/wishlist.html

[6] http://www.cuj.com/documents/s=8246/cujcexp2102alexandr/alexandr.htm

[7] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1385.htm

[8] http://www.boost.org/libs/mpl/doc/index.html

[9] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1489.pdf

[10] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1791.pdf

[11] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1393.html

[12] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1758.pdf

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1782.pdf

[13] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1705.pdf

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1794.pdf

[14] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1603.pdf

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1704.pdf

[15] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1510.pdf

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1522.pdf

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1536.pdf

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1758.pdf

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1782.pdf

你可能感兴趣的:(编程,C++,c,框架,C#)