正文

[转] C++0x设计之路2007-10-02 14:39:00

【评论】 【打印】 【字体: 】 本文链接:http://blog.pfan.cn/lym51/29789.html

分享到:

C++0x设计之路  ————把握现在 展望未来

                                                 [] Bjarne Stroustrup

                                                                []firingme

[译者注:原文地址:http://www.research.att.com/~bs/rules.pdf]

每隔一两天,我都会收到关于改进C++e-mail建议。很多建议如果真的能够成为语言或者标准库的一部分将是一件不错的事情,会让很多程序员深受其益。当然,综合来看,这些建议有很多重复之处,因此,也可以说,在我提出的列表中仅有上百个建议而已——具体的数量取决于你对相关建议这个概念的定义。人们希望改进的语言特性的一份不完全列表在 http://www.research.att.com/~bs/evol-issues.html;而与之对应的要求标准库改进的不完全列表则在 http://lafstern.org/matt/wishlist.html。我对这两份列表的基本观点是:前者太长而后者太激进。

      现在,假设你就是ISO C++标准委员会负责人,有权决定下一代C++标准的内容,请你仔细思考:什么是可以加入的?什么是应该剔除的?什么是需要改变的?这不是开玩笑,请暂时中断阅读,严肃认真地权衡把握,然后作出一份改进列表。如果你在读完这篇文章之后仍然对你的结论感到满意,请把它们加上注释并且e-mail给我。

      在修订标准的过程中,有一个问题必须首先明确:我们希望取得什么样的结果?当然,这个问题很难有完美的答案;我们甚至不能准确地定义我们这个词的范围到底有多大!C++社群是一个庞大的集体——去年IDC的统计结果是300万人左右——而且这个集体内部还可以细分为很多更小的单位。我们使用现有的几乎全部的计算机和操作系统,与所有的应用软件协同工作,生活在世界上全部的国家。这种情况必然导致我们具有各自完全不同的需求和愿望,而对我而言,这就表示我必须特别小心并且遵循一些基本的原则,以避免在迎合一部分人需要的同时损害了另外一部分人的利益。

      在本文中,我将阐释在往C++0x——下一代C++标准——发展的过程中,我们所必须遵循的一些最主要的原则。很明显,这些原则都是[3]中的概要原则的具体发展和体现。这种情况并不是偶然的。我的目的是进一步加强C++中已经被证明了的最具威力的部分,而不是试图去发明一项新的标准进而带来一场翻天覆地的大变化。这就是说,我的目标并不是让标准C++继续成为一门充满活力的语言,C++应该成长,应该适应,应该在我们整个社群现在和将来所面临的各种问题前,成为一件更有效率的工具。在这里,请注意我将行文语气转换成了第一人称,我想强调的是,我只不过是一个拥有很多成员的标准委员会中的一员。C++社群提出了大量的覆盖很多领域的观点,尽管我所持的观点是由大部分成员的认同所支撑的,可是这里给出的原则及其在现实世界中的应用仍然需要进一步的讨论、阐释和试验。这也是对待它们本应有的态度。

      那么,为什么我们不干脆接受所有合情合理的建议,让大家皆大欢喜呢?因为现在已经有了太多的建议等待我们去分析并给出详细而充分的说明。如果我们接受所有的建议的话,那完成这项工作将需要67年,而不是23年而已;并且将由此产生大量的新特性需要程序员们重新学习,而大部分程序员都将会偏安于一个较小而自我感觉舒适的子集之内,对大部分新特性视而不见;而且这样做的结果将会对零开销原则提出严峻的挑战——众多的特性将要求实现者耗费巨大的努力去实现新的标准,而在实现各个不同的特性时又会产生交互影响,还有很多建议的提出根本没有考虑效率因素,并需要一整套复杂而精致的运行时机制对其提供支持。不仅如此,上述一切还是建立在各编译器提供商愿意实现这个新标准的基础上——而我对他们是否愿意还完全没有把握。所以,在你下次想要提出仅加上我这两个新特性吧这样的要求前,请仔细考虑上面这些利害关系,三思而后言。

设计和演化

      C++0x将会100%兼容现有的C++标准。如果你现有的代码符合C++98标准,那么在过渡到C++0x的过程中,它们将有非常好的可移植性。对这个最好的保证就是标准委员会需要对比你手头更多的旧代码负责。尽管如此,我们也并未固步自封,我们明白C++的进一步进化是必要的,而且我们都希望未来的代码能够更简单、更优雅、更易维护,甚至——更高效。

      我们的目的是兼容,可我们也认识到,在某些情况下,付出微小的不兼容的代价的确能换来巨大的进步。一个明显的例子就是加入一个新的关键字。我们试图避免不兼容,可绝对的没有不兼容就意味着没有改变或者让新特性都具有丑陋的语法形式。就我个人而言,我极度不喜欢一些折衷、将就的做法。例如:利用宏提供一个__XXX关键字:

#define   XXX  __XXX

而不是直接加入一个新的XXX关键字。这种折衷是为那些持有包含XXX的代码而又无法对其进行修改的人提供的,而这个折衷确是以社群中其他所有人的不便为代价的,并且它给学习者增加了复杂度,并且给那些不太喜欢C++的人又提供了一个廉价的谈资。当然,前面所说的那种情况的确是存在的,可你仍然有别的选择——不升级或者通过一个编译器开关获得向后兼容性。

通常会认为,语言的演化和语言的设计是两件相去甚远的事情。而我想指出这种观点是错误的,语言的演化就等同于语言的设计。只不过,语言的演化和通常想象中的 白手起家式的语言设计并不相同,它既要受到现存的大量代码的约束,同时也可以从一些将现有经验直接应用的过程中(通过反馈)受益良多。保证新标准的兼容性是一件非常困难的工作,可我们能籍此使得数量巨大的社群成员轻松地过渡到新标准,并能使得新旧特性之间能够互动交流,平滑合作。选择这种设计式的演化方式,我们也能避免由于我们自身的经验和眼界所限而扼杀一些非常有用的程序设计技术的情况的出现。

如果沉溺于一些微不足道的形式符号表现的问题的话,那语言的设计工作就将变得杂乱无章,毫无条理。一门语言中,最明显最重要的特性就是那些能给其用户提供一种新的更有效率的编程风格的特性。就C++而言,这就意味着语言本身和标准库的变化必须能将一些崭新的编程和设计技术带入主流群体(包括工业界和教育界)的使用之中。只有那些能改变我们思维方式的特性才是值得提供的。

总结:C++0x的目标是在高度可移植的强力限制下做出的一种演化。这种演化应该能在现实世界中带来巨大的改进。

一般和特殊

C++对于常规特性(主要就是类)的突出强调是它的主要威力。

      对一些特定的功能的要求和对一些符号表达进行改进是非常常见的改革建议。可就算这些建议真地能够成为标准,也并不会给大家带来多少快乐。毕竟,如果一项特性只不过是针对一个特定问题的直截了当的解决方案的话,那么它通常和系统的其他部分并没有多大联系,它会非常容易解释,实现起来也不会有太大困难,并且针对一些精心挑选的代码,它可以有逻辑上最小的表现形式。那些喜欢拿语言做比较的人就经常使用一些这样子的功能清单。这种观点的问题是,我们所需要面对的问题在本质上是无穷尽的,这就要求我们提供一个无穷尽的功能集合。Pascal中的过程参数和C#中的委托机制就是一个明显的例子。与之不同的是,C++(在之前是K&R C)的传统做法一般是提供一些通用的特性集合,让优秀的程序员能够对很大一部分的问题构造解决方案。指针和类的概念就是明证。

      C++对于通用特性的的强调已经成为其最具威力的方面;而同时,由此带来的在某些特定功能(例如属性和线程)上的缺乏也已被认为是C++最薄弱的环节。很显然,这两个观点都是正确的。不过,我们仍然要对那些能提供更有力的抽象手段的的通用特性给予强烈的关注;C++社群的多样性决定了这一点。如果要为某些特定领域——例如Windows程序或者嵌入式系统程序——提供一些精心剪裁的特殊的功能,而这些功能又仅能为它们所特定的领域服务的话,那这些功能将会是一个承重的负担C++0x不会成为一门“Windows语言,或者“web语言,当然更不可能是嵌入式语言。它将是一门通过一些公用的基础设施来支持所有这些领域——而且范围更大——的通用型语言。

      对通用机制的偏爱,一个很重要的原因就是这种机制通常可以为那些现在还完全意料不到的问题提供解决方案,而特殊机制则做不到这一点。可以肯定的是,未来肯定会有一些令人措手不及的变故发生。我可不希望一门语言只能表达那些在它的设计阶段就已经明确指定 的领域。

      C++变得更加通用的一个很明显的可改进点就是为范型编程提供更好的支持以及更灵活的初始化/构造机制(示例中有进一步解释)。另一个明显的可改进点就是在并行、并发、分布式计算越来越普及的今天,为它们提供某种支持。种种不同的途径和技术表明了不存在一种单独的机制可以覆盖所有类型的应用。因此,一种明显的做法就是提供一些很简单的语言机制并且与程序库(利用范型和面向对象技术构建)相配合。

      总结:C++0x的目标是提供通用型的语言机制,这些机制可以自由组合,并针对某些特定的领域提供构建于这些通用机制之上的标准程序库。

专家和新手

      C++有一种转变为专家专用的语言的趋势。而在专家群(个人组合或者是通过网络组合)中,对于如何帮助新手,则很难建立广泛的共识,有时甚至对此无人问津。一个典型而普遍的观点(在这样的群体里)是:帮助新人最好的办法就是让他成为专家。可问题是,成为专家需要花费大量的时间,而大多数人在这个过程中,还必须进行一些实际的工作,并被要求保持一定的工作效率。更有趣的是,很多C++的新手对于成为C++的专家毫无兴趣。如果你是一个物理学家,只是碰巧需要做一些计算性的工作;或者是一名商务人员,只不过因工作需要要接触一些软件上的事务;甚或还是一个正在学习程序设计的学生,你只希望学习那些能让你完成工作的语言设施。你并不想成为语言专家——你只希望在你自己的领域内成为专家,并且了解一些能让你完成工作的语言特性就可以了。在有合适的程序库的支持下,C++可以做到这一点——而且它也的确被这样广泛使用。当然,这样做会有陷阱,会有缺陷,并且传统的教育手段给这种C++临时使用增添了不必要的困难度。而利用一些最新的工作成果,C++0x可以在这个上面做得更好。

      请考虑一些极其平凡的例子。你曾经写过这样的或者类似的代码吗?

vector<vector<double>> v;

或者

int i = extract_int(s);       // s is a string, e.g. "12.37"

或者

vector<int>::iterator p = find(tbl.begin(),tbl.end(),x);     // tbl is a const vector<int>

如果你从未写过类似的代码,那我想你要么是以另外一种同样有自己问题的方式来使用C++,要么就是你很少使用C++。允许>>符号作为两个模板参数的终结符可以解决第一个问题;为string提供一个标准的数值解析操作可以让新人们免去查找形如extract_int()这样的函数定义之苦;而第三个问题的解决方案,则是让p的类型能够由其初始化形式自动推导得出,就像下面这样:

auto p = find(tbl.begin(),tbl.end(),x);

// tbl is a const vector<int>

// p becomes a vector<int>::const_iterator

      可以证明,上述的>>auto这两个解决方案是符合C++0x的原则的。而为任何程度的新手提供帮助,则要求语言本身和标准库共同努力。并且对教育的关注将会是核心议题;相关讨论,可以参考[4]

      而根据concepts的重载(请参考示例部分),则可以得到一个更为简洁的的方案:

auto p = find(tbl,x);

// tbl is a const vector<int>

// p becomes a vector<int>::const_iterator

      为帮助新人而加入的特性(包括所有层次的技术和意见)不应与语言其它部分隔离而治,自成一派,形成一些新人卫星城一类的东西。和所有的特性一样,他们应该能在主要的系统内得到应用,和其他特性平滑互动,并且为进一步学习完整的语言和标准库提供一条道路。

      总结:C++0x必须为各种程度的初学者提供比现有的C++更好的支持——这包括减少易错的语言特性和提供更多的支持库。

类型安全

对于缺乏类型安全的问题,最直接的解决方案就是提供一个以静态类型为基础的带范围检查的标准库,并且在C++的教学中以其为基础。

      优雅和高效的代码的秘诀就是建立在灵活和易扩展的类型系统之上的类型安全。C++0x不可能使得C++达到完全的类型安全——这样将会导致数组、未初始化指针、联合、窄向转型、C风格的链接以及更多的功能都不可使用。这样做同样会让在很多嵌入式系统中所必需的对硬件的存取操作产生问题。那么,我们能怎么做?类型安全的缺乏是很多易错的,效率不佳的问题的根源所在。例如:

      void get_input(char* p)

      {

          char ch;

          while (cin.get(ch) && !iswhite(ch)) *p++ = ch;

          *p = 0;

      }

这段代码就足以让人心生寒意,惶惶然不知如何是好。同样的,如果你对效率极其关注,下面这种通用的链表的实现方法相信也难以让你满意:

struct Link {

    Link* link;

    void* data;

};

void my_clear(Link* p, int sz) // clear data of size sz

{

    for (Link* q = p; q!=0; q = q->link) memset(q->data,0,sz);

}

我们该怎么办?基本上,我们所能做的,就是为那些不安全的操作提供其他的可选途径,并且依靠工具的帮助来检测那些不安全的(通常也是传统的)用法。例如:

string s;

cin >> s;

这个替代品完美地解决了前面的那个低级而臃肿的get_input()所具有的问题;而对于第二个问题,既明了又高效(至少具有高效的潜力)的解决方案很干净利落:

template<class In> void my_stl_clear(In first, In last)

{

    while (first!=last) *first++ = 0;

}

这个可以作为my_clear()的一个完美的替代品。my_clear()的问题在哪?当然,从代码行数而言,它比my_stl_clear()更长一些,不过在考虑效率问题的时候,这并不是关键所在。问题的关键在于它缺乏类型信息。这意味着编译器必须假设在往q->data写入时(也就是memset中),*(q->link)的内容甚至q->link本身都有可能改变。这基本上就让优化器在此毫无用武之地。而一旦我们能够静态地确定datalink是两种不同类型,编译器就可以认为这两者不会相互影响。所以,在有一个还可接受的优化器的情况下,my_stl_clear()应该要比my_clear()快很多倍。另外,由于我们使用了void*类型,为了存取其内容,我们不得不使用一些折衷的办法,memset()的使用就是一例:在这种情况下,我们最后经常被迫使用函数——甚至是间接调用的函数——来达到目的。而这样做,有时其开销是巨大的。

对于缺乏类型安全的问题,最直接的解决方案就是提供一个以静态类型为基础的带范围检查的标准库,并且在C++的教学中以其为基础。当然,这样做也无法消灭全部的类型错误——很多时候,程序员会选择采用硬编码的原语的风格来写程序,这样做的原因有很多,而在程序员决定要这样做的时候,他们则必须关闭检查。另外,还存在另外的很多很多标准库尚未涵盖的领域。而这个问题的解决则要求标准库设立一个标准,要求其他的类库遵照此标准与其协同工作。

一个系统地采用范围检查的标准库必然要求系统地使用异常,这对于那些实时性要求不甚严格的程序而言并没有问题。而且现在我们也已经知道应该如何处理异常了(请参考[5]的附录E)。

总结:C++0x并不能完全填补现有C++的类型体系的漏洞,可它将不会引入新的漏洞,并且将会提供一些避免那些先天不足的设施的方法——主要是通过标准库提供一套类型安全(编译时或者运行时)的替代品。

效率和机器模型

通过将语言特性简单映射为硬件功能获得高效率是C++成功的核心(C也一样)。有个明显的例外就是使用异常时C++将要求一个最小的运行时支持。RTTIdynamic_casttypeid)和自由存储(newdelete)仅在你直接或间接使用它们时才被包含进来。这种可以消灭潜在开销的能力在某些应用领域——主要是嵌入式领域——是极其重要的。尽管近年来,技术上已经能越来越高效地处理异常问题,可在某些实时性要求极高的领域,其结果还是难以预期。在必要的时候,可以通过编译器开关禁止异常的使用。

C++0x中没有任何事物将会改变现有的状况,C++以前是,并且C++0x将会继续是那些追求最高效率以及最小资源消耗的应用的首选语言。如果C++0x中加入了一些需要运行时支持的特性,那这些特性也肯定会被设计成只有在代码中真正使用了的时候,才会要求获得额外的支持。这就是,著名的“0开销原则:不使用,不负责以及媲美手写,这条原则将仍然是C++最根本的信条。

C++的硬件模型非常简单:基本的类型直接映射为硬件能够识别的实体,例如:字节、字以及寄存器;对象间的联系直接映射为硬件间的联系;各种操作直接映射为机器指令;任何操作都可以直接完成,不需要额外的分配、间接性以及不必要的七弯八绕。现在的挑战是如何将并发加入这幅图画中。

总结:C++0x将继续遵循“0开销原则,并且继续保持能够直接而有效的使用所有硬件的机器模型。

语言和程序库

并不是每个库都必须进入标准。

      一门语言不可能覆盖任何事物,可是一个巨大的程序库可以做到这一点。并不是每个库都必须进入标准。C++社群有充足的空间来容纳一个大的标准库和一个工业库(商业的和开源的都可以)。标准委员会的程序库技术报告(Library TR[7])通过增加正则表达式和哈希表等基础设施的方式扩充了标准库的内容。Boost社群则对什么东西是可能成为标准的给出了另外的例子。然而,甚至这种形式的扩充也无法满足我的胃口。我希望看到C++标准库成为一个系统程序设计的更具弹性的平台,这当然也应该包括对并发和地理上的分布式计算的支持。分布式也许超出了社区能力的限制——毕竟我们也只不过是一群有自己的日常工作并且没有多余的经费支持开发的志愿者。尽管如此,我们能够,也必须,怀有梦想。

      程序库是一个社区能够承受得起的,能自由探索并有望收获幸运的领域。在这里,社区能够提供大量的(非常重要的)支持。一个程序库和一个语言特性的关键性区别就在于它并不是独占式的:如果你不喜欢一个库,换一个就是。而且,程序库可以在和编译器提供商完全无关的情况下,完全自由地进行开发、测试甚至是部署。C++为各程序库提供的非常富有表现力的机制——甚至在效率要求严格和资源严重受限领域——不应该被低估。现有的例子可以参看Boost [8]C++0x应该能让建立程序库更有效率。

      一个最常见而普遍的对C++的要求就是一个标准的GUI库。然而技术上、经济上还有政治上对其阻碍力量是巨大的。尽管如此,回顾过去,我们也曾遭遇过对在C++98中融入一个优秀的容器和算法库的巨大阻碍。可结果STL出现了。我希望发生一个奇迹,梦想着能有一个标准的界面将现有的各种商业的和开源的GUI设施标准化。这不能算一个合理的梦想,可正如很多人指出的一样,世界从来不为循规蹈矩的人而改变。

      总结:扩充程序库比扩充语言要好。我们对待语言扩充要谨慎小心,多方求证;而在对待一个新的程序库——特别是当这个库能够扩展系统程序设计的可移植性的时候——我们则应该勇敢果断,敢于探索。

标准和现实

      C++社区而言,ISO标准委员会是重要的,可很明显,它也仅仅是影响C++程序发展的诸多力量中的一小部分。我在此并未提到其他的语言,可C++的天性就是能够与大量系统和程序库协同工作。而这一点,影响了我们对语言特性和程序库的看法。更进一步地说,很多语言改进建议和库扩充建议或多或少都是以X语言一样做这样的要求开始的。我想也许几乎所有语言的较优秀的特性几乎都,在不同时间,曾被建议加入C++中。作为教授,很多标准委员会的成员都对大量的程序设计语言非常熟悉,而我们的经验又会指引着我们做设计决策。请参考[9],那里阐述了C++ISO C之间一些特定而微妙的议题。

      C++0x的发展必须及时,而且得到编译器产商的认同,新的语言特性实现起来必须比C++98中最难的容易。

      作为程序员——而且是一个在多种系统上工作的程序员——我们并不喜欢语言中的方言。可这些方言曾经而且也将继续存在。这些非标准的特性主要来源于与操作系统、数据库、中间件的绑定;支持准标准是另外的一个原因;而第三个原因就是为了对一小部分人群提供支持,而所提供的特性与C++社区的绝大部分并无关系。我的建议是:只要可能,请尽量避免使用非标准的特性。当避无可避时——这种情况在很多主要系统中都存在——我建议将那些非标准的用法局部化并通过ISO C++为其提供一个标准的存取界面。C++对通用抽象机制的改善很大一部分原因就是让包装这类非标准代码更容易一些。作为替代,可以使用编译器提供的lock-in机制。(The alternative is vendor lock-in

一个例子:范型编程支持

考察下面的代码:

template<class T> class vector {

    // ...

    void push_back(const T&) { /* _ */ }

    // ...

};

vector<double> v;

v.push_back(1.2);

v.push_back(2.3);

v.push_back(3.4);

这里基本上就是对模板和STL的应用。通过大规模的现实世界的应用和大量的实验,我们可以对那些成功的非常有弹性的语言特性和编程技术进行深入的思考;请参考STLBoost。特定地,在效率成为关键问题时,模板已经成为了标准的解决方案。那么,在C++0x中我们如何才能做得更好?这里的更好,我的意思是如何在与C++98相较没有效率损失的情况下,获得更优雅的表现形式;而现有的方式又有何问题?说得更具体一点,上面给出的这个vector的例子是否能够进一步改善?那些重复使用的push_back()即烦人又丑陋,而没有明确指出元素类型则让出错信息复杂难解,更完美的情况下,我还不想暴露push_back()的实现。一个更好的版本的代码可以是这样的:

template<Value_type T> class vector {

    // ...

    void push_back(const T&); // just the declaration

    // ...

};

vector<double> v;

v.push_back(1.2);

v.push_back(2.3);

v.push_back(3.4);

这里,使用Value_Type来指定元素类型T是一个“concept”;指明了vectorT所作的所有假设。在此基础上,我们可以校验double是一个Value_type,我们可以在没有push_back()定义的情况下对v作类型检查。当然,如果我们想要在线化,那我们需要定义push_back(),可这已经成为实现细节,无需考虑了。Concepts的概念使得我们可以在各转换单元内独立地进行类型检查,而无需引入模板参数的一整套的类型信息;并且有助于提高使用模板时的编译期效率。

那我们如何才能移除对push_back()的重复调用呢?我们可以允许vector利用一个初始化链作为其参数。这要求提供一个以初始化链做参数的构造函数。例如:

template<Value_type T> class vector {

    // ...

    vector(const T*, const T*); // sequence constructor

    // ...

};

vector<double> v = { 1.2, 2.3, 3.4 };

      concepts和通用的初始化链的细节问题仍然在紧张的讨论之中。这里的重点不是细节,而是我认为这些通用设施与C++0x实际上休戚相关。

会发生吗?

       我认为C++0x将会很清晰地展示出这里给出的“概要原则”。然而,资源的匮乏(时间,人力以及其他)限制了我们的能力,并且很明显,我们将会犯一些错误。同样的,一些“随机扩充”也会趁机混入其中,在整个语言中形成一些“古怪的孤岛”(就如同CC++中的枚举一样多!)。尽管如此,对于整个C++用户群及其更多的将来的用户而言,仍然有理由期待,C++0x将是对现有C++的一个重大的改进。

阅读(4197) | 评论(0)


版权声明:编程爱好者网站为此博客服务提供商,如本文牵涉到版权问题,编程爱好者网站不承担相关责任,如有版权问题请直接与本文作者联系解决。谢谢!

评论

暂无评论
您需要登录后才能评论,请 登录 或者 注册