longshanks

  C++博客 :: 首页 :: 联系 :: 聚合  :: 管理
  11 Posts :: 0 Stories :: 70 Comments :: 0 Trackbacks

2008年2月26日 #

     摘要: C++的营养 莫华枫     上一篇《C++的营养——RAII》中介绍了RAII,以及如何在C#中实现。这次介绍另一个重要的基础技术——swap手法。 swap手法     swap手法不应当是C++独有的技术,很多语言都可以实现,并且从中得到好处。只是C++存在的一些缺陷迫使大牛们发掘,并开始重视这种有用的手法。这 个原...  阅读全文
posted @ 2008-02-26 15:16 longshanks 阅读(1383) | 评论 (3)编辑 收藏

2008年2月16日 #

     摘要: C++的营养 莫华枫     动物都会摄取食物,吸收其中的营养,用于自身生长和活动。然而,并非食物中所有的物质都能为动物所吸收。那些无法消化的物质,通过消化道的另一头(某些动 物消化道只有一头)排出体外。不过,一种动物无法消化的排泄物,是另一种动物(生物)的食物,后者可以从中摄取所需的营养。    一门编程语言,对于程序...  阅读全文
posted @ 2008-02-16 08:19 longshanks 阅读(1221) | 评论 (2)编辑 收藏

2008年2月14日 #

瓦格纳的排场

    这个春节过的实在无趣。走完亲戚,招待完亲戚,逛街买好东西,就没多少时间了。看书的兴致也没了。想写点什么,总是没法集中精力。实在腻味了,把以前下载的瓦格纳的歌剧《尼伯龙根指环》拿出来看看。自从下载,没怎么好好看过,这回算是补上了。
    瓦格纳的“指环”系列可以算是歌剧里的极品,总共四出:莱茵黄金、女武神、齐格佛雷德和众神的黄昏。分成四个晚上连演,总共加起来大约15个小时。不说别 的,里面的角色众多,光神就有8个,人有7个,女武神9个,尼伯龙根矮人2个,还有三个仙女、2个巨人,和一只小鸟(在后台的女高音)。情节错综复杂,音 乐更是宏大。资料上说,这部歌剧中有200多个动机组合、交织在一起。不过,这还不能表现出瓦格纳在音响上近乎变态的追求。指环系列要求一个超过100人 庞大的乐团,并且引入了几种新的乐器,包括 Wagner tuba, bass trumpet和contrabass trombone。其中, Wagner tuba还是他为这部歌剧专门发明的。最夸张的是,瓦格纳为了获得如此庞大的乐队同演员声音之间的平衡,专门建造了一座歌剧院,也就是著名的拜罗伊特节日剧院(Bayreuth Festspielhaus)。这在音乐史上是绝无仅有的。正是由于这种骇人的排场,造就了歌剧史上的巅峰之作。
    这倒让我联想到C++。说实在的,C++的使用,有时也同瓦格纳谱写的歌剧那样,非常复杂、庞大,需要大量的投入,和前期准备。有时,为了一些应用,而去构造一些的基础设施(就像
拜罗伊特节日剧院)。这种庞大导致了应用面的狭窄,但是却能够获得极品般的东西。这种东西当然不会是到处都有,但却是强大的、伟大的,以及关键性的。
    当然,排场仅仅是表面的东西,真正吸引人的,还是瓦格纳的音乐。歌剧在瓦格纳手里,不再是一系列的咏叹调。瓦格纳是在用音乐讲故事。音乐是歌剧的一部分, 歌唱是音乐的一部分,布景、灯光和舞台效果都是不可分割的一分子。所有这些是一个整体,除了个别出彩的乐段(基本上只有“飞驰的女武神”这一段,曾被用在 电影“现代启示录”中),很少单独演奏。它们戏剧性太强了,脱离了歌剧,就仅仅是一堆音符而已。
    这一点上,C++也是如此,语言、库、惯用法等等,都是整体,一旦相互脱离,便无法发挥应有的作用。所以孤立地运用C++某一方面的特性,往往会误入歧途,只有综合运用各种手段,才能真正地用好C++。
    瓦格纳庞大复杂和莫扎特的简洁优雅形成了鲜明的对比。但是我们不能说莫扎特比瓦格纳更好,或者反过来。他们的音乐都是最伟大的杰作,一个人可以毫无冲突地 同时成为他们俩人的粉丝(就像我:))。我们总能从中获得想像、思考、思想和伦理,两者都具备无法替代的营养。认真地学习和吸收,才是正道。
    编程语言的学习和使用,又何尝不是如此呢?

注:严格地说,瓦格纳的这些作品并不是歌剧,有一个正式的名称:music drama,直接翻译是“音乐戏剧”,是歌剧的扩展。但是为了方便,还是广义上地将其称作歌剧。
posted @ 2008-02-14 11:25 longshanks 阅读(729) | 评论 (1)编辑 收藏

2008年1月25日 #

当GPL遇上MP

莫华枫

    GPL,也就是General Purpose Language,是我们使用的最多的一类语言。传统上,GPL的语法,或者特性,是固态的。然而,程序员都是聪明人(即便算不上“最聪明”,也算得上 “很聪明”吧:)),往往不愿受到语法的束缚,试图按自己的心意“改造”语言。实际上,即便是早期的语言,也提供了一些工具,供聪明人们玩弄语法。我看的第一本C语言的书里,就有这么一个例子,展示出这种“邪恶”的手段:
      #define procedure void
      #define begin {
      #define end }
    然后:
      procedure fun(int x)
      begin
          ...
      end
    实际上,它的意思是:
      void fun(int x)
      {
          ...
      }
    这可以看作是对初学C语言的Pascal程序员的安慰。这种蹩脚的戏法可以算作元编程的一种,在一种语言里创造了另一个语法。不过,实在有些无聊。然而,在实际开发中,我们或多或少地会需要一些超出语法范围的机制。有时为了完善语言,弥补一些缺憾;有时为了增强一些功能;有时为了获得一些方便。更新潮的,是试图在一种GPL里构建Domain Specific Language,或者说“子语言”,以获得某个特性领域上更直观、更简洁的编程方式。这些对语言的扩展需求的实现,依赖于被称为Meta- Programming(MP)的技术。
    另一方面,随着语言功能和特性的不断增加,越来越多的人开始抱怨语言太复杂。一方面:“难道我们会需要那些一辈子也用不到几回的语言机制,来增加语言的复杂性和学习使用者的负担吗?”。另一方面:“有备无患,一个语言机制要到迫在眉睫的时候才去考虑吗?”。但MP技术则将这对矛盾消弭于无形。一种语言,可以简洁到只需最基本的一些特性。而其他特定的语言功能需求,可以通过MP加以扩展。如果不需要某种特性,那么只要不加载相应的MP代码即可,而无需为那些机制而烦恼。
    MP最诱人的地方,莫过于我们可以通过编写一个代码库便使得语言具备以往没有的特性。
    然而,全面的MP能力往往带来巨大的副作用,以至于我们无法知道到底是好处更多,还是副作用更多。语言的随意扩展往往带来某些危险,比如语法的冲突和不兼容,对基础语言的干扰,关键字的泛滥等等。换句话说,MP是孙悟空,本领高强。但没有紧箍咒,是管不住他的。
    那么,紧箍咒是什么呢?这就是这里打算探讨的主题。本文打算通过观察两种已存在的MP技术,分析它们的特点与缺陷,从中找出解决问题的(可能)途径。

AST宏

    首先,先来看一下宏,这种远古时代遗留下来的技术。以及它的后裔,ast宏。
    关于传统的宏的MP功能,上面的代码已经简单地展示了。但是,这种功能是极其有限的。宏是通过文本替换的形式,把语言中的一些符号、操作符、关键字等等替换成另一种形式。而对于复杂的语法构造的创建无能为力。问题的另一面,宏带来了很多副作用。由于宏的基础是文本替换,所以几乎不受语法和语义的约束。而且,宏的调试困难,通常也不受命名空间的约束。它带来的麻烦,往往多于带来的好处。
    ast宏作为传统宏的后继者,做了改进,使得宏可以在ast(Abstract Syntax Tree)结构上执行语法的匹配。(这里需要感谢TopLanguage上的Olerev兄,他用简洁而又清晰的文字,对我进行了ast宏的初级培训:))。这样,便可以创造新的语法:
      syntax(x, "<->", y, ";")
      {
          std::swap(x, y);
      }
    当遇到代码:
      x <-> y;
    的时候,编译器用std::swap(x,y);加以替换。实际上,这是将一种语法结构映射到另一个语法结构上。而ast宏则是这种映射的执行者。
    但是,ast宏并未消除宏本身的那些缺陷。如果x或者y本身不符合swap的要求(类型相同,并且能复制构造和赋值,或者拥有swap成员函数),那么 ast宏调用的时候无法对此作出检验。宏通常以预编译器处理,ast宏则将其推迟到语法分析之时。但是此时依然无法得到x或y的语义特征,无法直接在调用点给出错误信息。
    同时,ast宏还是无法处理二义性的语法构造。如果一个ast宏所定义的语法构造与主语言,或者其他ast宏的相同,则会引发混乱。但是,如果简单粗暴地将这种“重定义”作为非法处理,那么会大大缩小ast宏(以及MP)的应用范围。实际上,这种语法构造的重定义有其现实意义,可以看作一种语法构造的“重载”,或者函数(操作符)重载的一种扩展。
    解决的方法并不复杂,就是为ast宏加上约束。实际上,类似的情形在C++98的模板上也存在,而C++则试图通过为模板添加concept约束加以解决。这种约束有两个作用:其一,在第一时间对ast宏的使用进行正确性检验,而无需等到代码展开之后;其二,用以区分同一个语法构造的不同版本。
    于是,对于上述例子可以这样施加约束(这些代码只能表达一个意思,还无法看作真正意义上的MP语法):
      syntax(x, "<->", y, ";")
           where x,y is object of concept (has_swap_mem or (CopyConstructable and Assignable))
                && typeof(x)==typeof(y)
      {
          std::swap(x,y);
      }
    如此,除非x,y都是对象,并且符合所指定的concept,否则编译器会当即加以拒绝,而且直截了当。
    不过,如此变化之后,ast宏将不会再是宏了。因为这种约束是语义上的,必须等到语义分析阶段,方能检验。这就超出了宏的领地了。不过既然ast宏可以从预处理阶段推迟到语法分析阶段,那么再推迟一下也没关系。再说,我们关注的是这种功能,带约束的ast宏到底是不是宏,也无关紧要。

TMP

    下面,我们回过头,再来看看另一种MP技术——TMP(参考David Abrahams, Aleksey Gurtovoy所著的《C++ Template Metaprogramming》)。对于TMP存在颇多争议,支持者认为它提供了更多的功能和灵活性;反对者认为TMP过于tricky,难于运用和调试。不管怎么样,TMP的出现向我们展示了一种可能性,即在GPL中安全地进行MP编程的可能性。由于TMP所运用的都是C++本身的语言机制,而这些机制都是相容的。所以,TMP所构建的 MP体系不会同GPL和其他子语言的语法机制相冲突。
    实际上,TMP依赖于C++的模板及其特化机制所构建的编译期计算体系,以及操作符的重载和模板化。下面的代码摘自boost::spirit的文档:
      group = '(' >> expr >> ')';

      expr1 = integer | group;

      expr2 = expr1 >> *(('*' >> expr1) | ('/' >> expr1));

   expr = expr2 >> *(('+' >> expr2) | ('-' >> expr2));

    这里表达了一组EBNF(语法着实古怪,这咱待会儿再说):>>代表了标准EBNF的“followed by”,*代表了标准EBNF的*(从右边移到左边),括号还是括号,|依旧表示Union。通过对这些操作符的重载,赋予了它们新的语义(即EBNF的相关语义)。然后配合模板的类型推导、特化等等机制,变戏法般地构造出一个语法解析器,而且是编译时完成的。

    尽管在spirit中,>>、*、|等操作符被挪作他用,但是我们依然可以在这些代码的前后左右插入诸如:cin>> *ptrX;的代码,而不会引发任何问题。这是因为>>等操作符是按照不同的类型重载的,对于不同类型的对象的调用,会调用不同版本的操作符重载,互不干扰,老少无欺。

    但是,TMP存在两个问题。其一,错误处理不足。如果我不小心把第二行代码错写成:expr1 = i | group;,而i是一个int类型的变量,那么编译器往往会给出一些稀奇古怪的错误。无非就是说类型不匹配之类的,但是很晦涩。这方面也是TMP受人诟病的一个主要原因。好在C++0x中的concept可以对模板作出约束,并且在调用点直接给出错误提示。随着这些技术的引入,这方面问题将会得到缓解。

    其二,受到C++语法体系的约束,MP无法自由地按我们习惯的形式定义语法构造。前面说过了,spirit的EBNF语法与标准EBNF有不小的差异,这对于spirit的使用造成了不便。同样,如果试图运用TMP在C++中构造更高级的DSL应用,诸如一种用于记账的帐务处理语言,将会遇到更大的限制。实际上TMP下的DSL也很少有令人满意的。

    所以说,TMP在使用上的安全性来源于操作符复用(或重载)的特性。但是,操作符本身的语法特性是固定的,这使得依赖于操作符(泛化或非泛化)重载的TMP不可能成为真正意义上的MP手段。

    那么,对于TMP而言,我们感兴趣的是它的安全性和相容性。而对其不满的,则是语法构造的灵活性。本着“去其糟粕,取其精华”的宗旨,我们可以对TMP做一番改进,以获得更完整的MP技术。TMP的核心自然是模板(类模板和函数/操作符模板),在concept的帮助下,模板可以获得最好的安全性和相容性。以此为基础,如果我们将模板扩展到语法构造上,那么便可以在保留TMP的安全性和相容性的情况下,获得更大的语法灵活性。也就是说,我们需要增加一种模板——语法模板

      template<typename T>
      syntax synSwap=x "<->" y ";"
         require SameType<decltype(x), T> && SameType<decltype(y), T>
                && (has_swap_mem<T> || (CopyConstructable<T> and Assignable<T>)
      {
          std::swap(x, y);
      }

    这里我杜撰了关键字syntax,并且允许为语法构造命名,便于使用。而语法构造描述,则是等号后面的部分。require沿用自C++0x的concept提案。稍作了些改造,允许concept之间的||。

用户定义的语法

    如果比较带约束的ast宏和语法模板,会发现极其相似。实际上,两者殊途同归,展示了几乎完全相同的东西。ast宏和TMP分别位于同一个问题的两端,当它们的缺陷得到弥补时,便会相互靠拢,最终归结到一种形式上。 

    现在,通过结合两种方案的特色,我们便可以得到一个最终的MP构造,既安全,又灵活:

      syntax synSwap=x "<->" y ";";

      where

          (x,y is object)

          && SameType<x, y>

          && (has_swap_mem<x> || (CopyConstructable<x> and Assignable<x>))

      {

          std::swap(x, y);

      }

    我去掉了template关键字,在约束(where)的作用下,template和类型参数列表都已经没有必要了。同时,也允许直接将对象放入 concept:Assignable<x>,这相当于:Assignable<decltype<x>>。

    “is ...”是特别的约束,用来描述那些超越concept范畴的特性。“...”可以是关键字“object”,表明相应的标识符是对象;也可以是关键字 “type”,表明相应的标识符是类型;或者是关键字“concept”,指定相应的标识符是concept等等。操作符的重载所涉及的参数只会是对象,只需对其类型做出约束,因此concept便足够使用。但语法构造的定义则广大的多,它不仅仅会涉及对象,更可能涉及其它类型的语法要素。实际上,语法构造所面对的参数是“标识符”。那么一个标识符上可能具备的各种特性,都应当作为约束的内容。这里大致归纳出以下几点需要约束的特性:

  1. 分类。对象、类型、concept、函数等等。is ...;
  2. 来源。来自哪个namespace。from ...;
  3. 尺寸。类型大小。sizeof(x)>20;
  4. 从属。指定一个语法构造是否从属于其他语法构造(其他语法构造的一部分)。如果是从属语法构造,那么将不能单独出现在独立的语句中。更进一步,可以强行指定一个语法构造从属于某个语法构造,不允许在其他语法构造中使用。belong_to ...;
  5. 类型及对象间的关系。诸如继承、子对象、子类型、true typedef、别名、成员等等。
  6. ...

    也可以认为,语法构造的约束是concept的自然延伸。concept对类型做出约束,而语法构造的约束的对象,则是标识符。

    为了强化语法构造的创建能力,应当允许在语法构造的定义中使用BNF,或其他类似的语法描述语言。

    在语法构造约束的支援下,便可以化解一些子语言同宿主语言之间的语法冲突。比如,我们创建了一种类似spirit的EBNF子语言,可以按照标准的EBNF形式编写语法,构造一个语法解析器:

      syntax repeat1=p '+'

      where

          p is object of type(Parser)

      {...}

    这样便定义了一个后缀形式的+操作符,但仅仅作用于类型Parser的对象上。对于如下的代码:

      letter + number

    使用主语言的+(加),还是使用EBNF的+(重复,至少一次),取决于letter和number的特征(这里是类型):

      int letter, number;

      letter + number; //使用主语言的+,表示letter加number

      Parser letter, number;

      letter + number; //使用EBNF的+,表示一串letter后面跟一个number

    如此,便使得用户定义的语法不会同主语言的相同语法发生冲突。

    但是,语法构造的约束并不能完全消除所有的语法冲突。其中一种情况便是常量的使用。比如:

      'x' + 'y'

    它在宿主语言中,会被识别为两个字符的相加。但在BNF子语言中,则会是若干个字符'x'之后紧跟一个‘y'。由于没有类型、对象等代码实体的参与,编译器无法分辨使用哪种语言的语法规则来处理。解决的方法是使得常量类型化。这种做法在C++等语言中已经运用多年。通常采用为常量加前后缀:

      'x'b_ + 'y'b_

    以此代表BNF子语言的常量。常量的语法定义可以是:

      syntax bnf_const="'" letter "'" "b_";

      where

          is const of ... //...可以是某种类型

      {...}

    通过is const of约束,将所定义的常量与某个类型绑定。而通过类型,编译器便可以推断出应当使用那种语法对其处理。

    然而,尽管带约束的语法构造定义可以在很大程度上消除语法冲突和歧义,但不可能消除所有的语法矛盾。另一方面,结构相似,但语义完全不同的语法构造混合在一起,即便不引发矛盾,也会对使用者造成难以预计的混乱。因此,在实际环境下,需要通过某种“语法围栏”严格限定某种用户定义语法的作用范围。最直接的形式,就是通过namespace实现。namespace在隔离和协调命名冲突中起到很好的作用,可以进一步将其运用到MP中。由于namespace一经打开,其作用范围不会超出最内层的代码块作用域:

      {

          using namespace XXX;

          ...

      } //XXX的作用范围不会超出这个范围

    运用这个特性,可以很自然地将蕴藏在一个namespace中的用户定义语法构造限制在一个确定的范围内。

    但是,仅此不够。毕竟语法不同于命名,不仅会存在冲突,还会存在(合法的)混淆,而且往往是非常危险的。为此,需要允许在using namespace的时候进一步指定语法的排他性。比如:

      {

          using namespace XXX exclusive;

          ...

      }

    如果namespace中的用户定义语法与外部语法(宿主语言,或外围引用的namespace)重叠(冲突或混淆),外部语法将被屏蔽。更进一步,可以允许不同级别的排他:排斥重叠和完全屏蔽。前者只屏蔽重叠的语法,这种级别通常用于扩展性的用户语法构造(扩展主语言,需要同主语言协同工作。而且语法重叠较少);而后者则将外部语法统统屏蔽,只保留所打开的namespace中的语法(子语言在代码块中是唯一语言),这种级别用于相对独立的功能性子语言(可以独立工作的子语言,通常会与主语言和其他子语言间存在较大的语法重叠,比如上述BNF子语言等等)。

    为提供更多的灵活性,可以在完全屏蔽的情况下,通过特定语句引入某种不会引发冲突的外部语法。比如:

      {

          using namespace XXX exclusive full;

          using host_lang::syntax(...); //引入主语言的某个语法,...代表语法名称

          ...

      }

    通常情况下,子语言不会完全独立运作,需要同宿主语言或其他子语言交互,也就是数据交换。主语言代码可能需要将数据对象传递给子语言,处理完成后再取回。由于编译器依赖于标识符的特性(主要是类型)来选择语法构造。所以,一种子语言必须使用其内部定义的类型和对象。为了能够交换数据,必须能够把主语言的对象包装成子语言的内部类型。通过在子语言的namespace中进行true typedef,可以将主语言类型定义成一个新的(真)类型,在进入子语言作用域的时候,做显式的类型转换。另外,为了方便数据转换,还可以采用两种方法:将主语言类型map到一个子语言类型(相当于给予“双重国籍”),进入子语言的语法范围时,相应的对象在类型上具有两重性,可以被自动识别成所需的类型,但必须在确保不发生语法混淆的情况下使用;另一种稳妥些的方法,可以允许执行一种typedef,介于alias和true typedef之间,它定义了一个新类型,但可以隐式地同原来的类型相互转换。数据交换的问题较复杂,很多问题尚未理清,有待进一步考察。

总结

    作为MP手段,ast宏拥有灵活性,而TMP则具备安全性,将两者各自的优点相结合,使我们可以获得更加灵活和安全的语法构造定义手段。通过在用户定义的语法构造上施加全面的约束,可以很好地规避语法冲突和歧义。

    但是,需要说明的是,这里所考察的仅仅局限在用户定义语法构造的冲突和歧义的消除上。GPL/MP要真正达到实用阶段,还需要面对更多问题。比如,由于存在用户定义的语法构造,语法分析阶段所面对的语法不是固态的,需要随时随地接受新语法,甚至重叠的语法(存在多个候选语法,不同的候选语法又会产生不同的下级语法),这就使语法分析大大复杂;语法模式匹配被推迟到语义分析阶段,此前将无法对某些语法错误作出检验;一个语法构造的语义需要通过宿主语言定义,如何衔接定义代码和周边的环境和状态;如何为用户定义的语法构造设置出错信息;由于某些语法构造的二义性,如何判别语法错误属于哪个语法构造;...。此外还有一些更本质性的问题,诸如语法构造的重载和二义性是否会更容易诱使使用者产生更多的错误等等,牵涉到错综复杂的问题,需要更多的分析和试验。

    另外,作为MP的重要组成部分,编译期计算能力也至关重要。TMP运用了C++模板特化,D语言通过更易于理解的static_if等机制,都试图获得编译期的计算能力,这些机制在完整的MP中需要进一步扩展,而并非仅仅局限在与类型相关的计算上。其他一些与此相关的特性,包括反射(编译期和运行期)、类型traits等,也应作为MP必不可少的特性。

posted @ 2008-01-25 15:09 longshanks 阅读(847) | 评论 (4)编辑 收藏

2008年1月6日 #


GP技术的展望——道生一,一生二

by  莫华枫



    长期以来,我们始终把GP(泛型编程)作为一种辅助技术,用于简化代码结构、提高开发效率。从某种程度上来讲,这种观念是对的。因为迄今为止,GP技术还只是一种编译期技术。只能在编译期发挥作用,一旦软件完成编译,成为可执行代码,便失去了利用GP的机会。对于现在的多数应用而言,运行时的多态能力显得尤为重要。而现有的GP无法在这个层面发挥作用,以至于我这个“GP迷”也不得不灰溜溜地声称“用OOP构建系统,用GP优化代码”。

    然而,不久前,在TopLanguage group上的一次讨论,促使我们注意到runtime GP这个概念。从中,我们看到了希望——使GP runtime化的希望——使得GP有望在运行时发挥其巨大的威力,进一步为软件的设计与开发带来更高的效率和更灵活的结构。
    在这个新的系列文章中,我试图运用runtime GP实现一些简单,但典型的案例,来检测runtime GP的能力和限制,同时也可以进一步探讨和展示这种技术的特性。

运行时多态

    现在的应用侧重于交互式的运作形式,要求软件在用户输入下作出响应。为了在这种情况下,软件的整体结构的优化,大量使用组件技术,使得软件成为“可组装” 的系统。而接口-实现分离的结构形式很好地实现了这个目标。多态在此中起到了关键性的作用。其中,以OOP为代表的“动多态”(也称为 “subtyping多态”),构建起在运行时可调控的可组装系统。GP作为“静多态”,运用泛化的类型体系,大大简化这种系统的构建,消除重复劳动。另外还有一种鲜为人知的多态形式,被《C++ Template》的作者David Vandevoorde和Nicolai M. Josuttis称为runtime unbound多态。而原来的“动多态”,即OOP多态,被细化为runtime bound多态;“静多态”,也就是模板,则被称为static unbound多态。
    不过这种称谓容易引起误解,主要就是unbound这个词上。在这里unbound和bound是指在编写代码时,一个symbol是否同一个具体的类型 bound。从这点来看,由于GP代码在编写之时,面向的是泛型,不是具体的类型,那么GP是unbound的。因为现有的GP是编译期的技术,所以是 static的。OOP的动多态则必须针对一个具体的类型编写代码,所以是bound的。但因为动多态可以在运行时确定真正的类型,所以是runtime 的。至于runtime unbound,以往只出现在动态语言中,比如SmallTalk、Python、Ruby,一种形象地称谓是“duck-typing”多态。关于多态的更完整的分类和介绍可以看这里
    通过动态语言机制实现的runtime unbound,存在的性能和类型安全问题。但当我们将GP中的concept技术推广到runtime时会发现,rungime unbound可以拥有同OOP动多态相当的效率和类型安全性,但却具有更大的灵活性和更丰富的特性。关于这方面,我已经写过一篇文章 ,大致描述了一种实现runtime concept的途径(本文的附录里,我也给出了这种runtime concept实现的改进)。

Runtime Concept

    Runtime concept的实现并不会很复杂,基本上可以沿用OOP中的“虚表”技术,并且可以更加简单。真正复杂的部分是如何在语言层面表达出这种runtime GP,而不对已有的static GP和其他语言特性造成干扰。在这里,我首先建立一个基本的方案,然后通过一些案例对其进行检验,在发现问题后再做调整。
    考虑到runtime concept本身也是concept,那么沿用static concept的定义形式是不会有问题的:
      concept myconcept<T> {
          T& copy(T& lhs, T const& rhs);
          void T::fun();
          ...
      }
    具体的concept定义和使用规则,可以参考C++0x的concept提案这篇文章 ,以及这篇文章
    另一方面,我们可以通过concept_map将符合一个concept的类型绑定到该concept之上:
      concept_map myconcept<MyType> {}
    相关内容也可参考上述文件。
    有了concept之后,我们便可以用它们约束一个模板:
      template<myconcept T>void myfun(T const& val); //函数模板
      template<myconcept T>class X  //类模板
      {
           ...
      };
    到此为止,runtime concept同static concept还是同一个事物。它们真正的分离在于使用。对于static concept应用,我们使用一个具体的类型在实例化(特化)一个模板:
      X<MyType> x1;  //实例化一个类模板
      MyType obj1;
      myfun(obj1);  //编译器推导obj1对象的类型实例化函数模板
      myfun<MyType>(obj1);  //函数模板的显式实例化
    现在,我们将允许一种非常规的做法,以使runtime concept成为可能:允许使用concept实例化一个模板,或定义一个对象
      X<myconcept> x2;
      myconcept* obj2=new myconcept<MyType>;
      myfun(obj2);  //此处,编译器将会生成runtime版本的myfun
    这里的含义非常明确:对于x2,接受任何符合myconcept的类型的对象。obj2是一个“动态对象”(这里将runtime concept引入的那种不知道真实类型,但符合某个concept的对象称为“动态对象”。而类型明确已知的对象成为“静态对象”),符合myconcept要求。至于实际的类型,随便,只要符合myconcept就行。
    这种情形非常类似于传统动多态的interface。但是,它们有着根本的差异。interface是一个具体的类型,并且要求类型通过继承这种形式实现这个接口。而concept则不是一种类型,而是一种“泛型”——具备某种特征的类型的抽象(或集合),不需要在类型创建时立刻与接口绑定。与 concept的绑定(concept_map)可以发生在任何时候。于是,runtime concept实际上成为了一种非侵入的接口。相比interface这种侵入型的接口,更加灵活便捷。
    通过这样一种做法,我们便可以获得一种能够在运行时工作的GP系统。
    在此基础上,为了便于后续案例展开,进一步引入一些有用的特性:
  1. 一个concept的assosiate type被视为一个concept。一个concept的指针/引用(concept_id*/concept_id&,含义是指向一个符合concept_id的动态对象,其实际类型未知),都被视作concept。一个类模板用concept实例化后,逻辑上也是一个concept。
  2. 动态对象的创建。如果需要在栈上创建动态对象,那么可以使用语法:concept_id<type_id> obj_id; 这里concept_id是concept名,type_id是具体的类型名,obj_id是对象名称。这样,便在栈上创建了一个符合concept_id的动态对象,其实际类型是type_id
    如果需要在堆上创建动态对象,那么可以用语法:concept_id* obj_id=new concept_id<type_id>; 这实际上可以看作“concept指针/引用”。
  3. concept推导(编译期)。对于表达式concept_id obj_id=Exp,其中Exp是一个表达式,如果表达式Exp的类型是具体的类型,那么obj_id代表了一个静态对象,其类型为Exp的类型。如果表达式Exp的类型是concept,那么obj_id是一个动态对象,其类型为Exp所代表的concept。
    那么如何确定Exp是具体类型还是concept?可以使用这么一个规则:如果Exp中涉及的对象,比如函数的实参、表达式的操作数等等,只要有一个是动态对象(类型是concept),那么Exp的类型就是concept;反之,如果所有涉及的对象都是静态对象(类型为具体的类型),那么Exp的类型为相应的具体类型。同样的规则适用于concept*或concept&。
  4. concept转换。类似在类的继承结构上执行转换。refined concept可以隐式地转换成base concept,反过来必须显式地进行,并且通过concept_cast操作符执行。兄弟concept之间也必须通过concept_cast转换。
  5. 基于concept的重载,也可以在runtime时执行,实现泛化的dynamic-dispatch操作。

    下面,就开始第一个案例。

案例:升级的坦克

    假设我们做一个游戏,主题是开坦克打仗。按游戏的惯例,消灭敌人可以得到积分,积分到一定数量,便可以升级。为了简便起见,我们只考虑对主炮升级。第一级的主炮是90mm的;第二级的主炮升级到120mm。主炮分两种,一种只能发射穿甲弹,另一种只能发射高爆弹。因此,坦克也分为两种:能打穿甲弹的和能打高爆弹的。
    为了使代码容易开发和维护,我们考虑采用模块化的方式:开发一个坦克的框架,然后通过更换不同的主炮,实现不同种类的坦克和升级:
      //一些基本的concept定义
      //炮弹头concept
      concept Warheads<typename T> {
          double explode(TargetType tt); //炮弹爆炸,返回杀伤率。不同弹头,对不同类型目标杀伤率不一样。
      }
      //炮弹concept,我们关心的当然是弹头,所以用Warheads定义一个associate type
      concept Rounds<typename T> {
          Warheads WH;
          ...
      }
      //主炮concept
      concept Cannons<typename T> {
          Rounds R;
          void T::load(R& r); //装填炮弹,load之后炮弹会存放在炮膛里,不能再load,除非把炮弹打出去
          R::WH T::fire();   //开炮,返回弹头。发射后炮膛变空,可以再load
      }
      //类型和模板定义
      //坦克类模板
      template<Cannons C>
      class Tank
      {
          ...
      public:
          void load(typenam C::R& r) {
              m_cannon.load(r);
          }
          typename C::R::WH fire() {
              return m_cannon.fire();
          }
      private:
          C m_cannon;
      };
      //主炮类模板
      template<Rounds Rd>
      class Cannon
      {
      public:
          typedef Rd R;
          void load(R& r) {...}
          typename R::WH fire() {...}
      }
      template<Rounds Rd> concept_map Cannons<Cannon<Rd>>{}
      //炮弹类模板
      template<Warheads W>
      class Round
      {
      public:
          typedef W WH;
          static const int caliber=W::caliber;
          W shoot() {...}
          ...
      };
      template<Warhead W> concept_map<Round<W>>{}
      //弹头类模板,通过traits把各类弹头的不同行为弹头的代码框架分离,使类型“可组装”
      concept WH_Traits<T> {
          return T::exploed(int, TargetType, double, Material);
      }
      template<WH_Traits wht, int c>
      class Warhead
      {
      public:
          const static int caliber=c;
          double explode(TargetType tt) {
              return wht::exploed(c, tt, ...);
          }
          ...
      };
      template<WH_Traits WHT, int c> concept_map<Warhead<WHT, c>>{}
      //弹头traits
      struct KE_WHTraits
      {
          static double exploed(int caliber, TargetType tt, double weight, Material m) {...}
      };
      concept_map<KE_WHTraits>{}
      struct HE_WHTraits
      {
          static double exploed(int caliber, TargetType tt, double weight, Material m) {...}
      };
      concept_map<HE_WHTraits>{}
      //定义各类弹头
      typedef Warhead<KE_WHTraits, 90> WH_KE_90;
      typedef Warhead<KE_WHTraits, 120> WH_KE_120;
      typedef Warhead<HE_WHTraits, 90> WH_HE_90;
      typedef Warhead<HE_WHTraits, 120> WH_HE_120;
      //定义各类炮弹
      typedef Round<WH_KE_90> Round_KE_90;
      typedef Round<WH_KE_120> Round_KE_120;
      typedef Round<WH_HE_90> Round_HE_90;
      typedef Round<WH_HE_120> Round_HE_120;
      //定义各类主炮
      typedef Cannon<Round_KE_90> Cannon_KE_90;
      typedef Cannon<Round_KE_120> Cannon_KE_120;
      typedef Cannon<Round_HE_90> Cannon_HE_90;
      typedef Cannon<Round_HE_120> Cannon_HE_120;
      //定义各类坦克
      typedef Tank<Cannon_KE_90> Tank_KE_90;
      typedef Tank<Cannon_KE_120> Tank_KE_120;
      typedef Tank<Cannon_HE_90> Tank_HE_90;
      typedef Tank<Cannon_HE_120> Tank_HE_120;
    于是,当我们开始游戏时,就可以按照玩家的级别创建坦克对象,并且射击:
      //第一级玩家,驾驶发射90mm高爆炮弹的坦克
      Tank_HE_90 myTank;
      Round_HE_90 r1;
      myTank.load(r1);
      myTank.fire();
      //第二级玩家,驾驶发射120mm穿甲弹的坦克
      Tank_KE_120 yourTank;
      Round_KE_120 r2;
      yourTank.load(r2);
      yourTank.fire();
      //如果这样,危险,炮弹不匹配,小心炸膛
      myTank.load(r2); //error
    到目前为止,这些代码仅仅展示了静态的GP。concept在这里也只是起到了类型参数约束的作用。但是,在这些代码中,我们可以明显地看到,在运用GP 的参数化类型特性之后,可以很容易地进行组件化。对于一组具备类似行为和结构特征的类型,我们可以通过模板的类型参数,将差异部分抽取出来,独立成所谓的 “traits”或者“policy”。并且通过traits或policy的组合构成不同的产品。在某些复杂的情况下,traits和policy还可以进一步通过traits或policy实现组件化。
    接下来,很自然地应当展开runtime GP的运用了。
    一个游戏者是可以升级的,为了使得这种升级变得更加灵活,我们会很自然地使用Composite模式。现在,我们可以在Runtime concept的支援下实现GP版的Composite模式:
      //坦克的concept
      concept tanks<T> {
          typename Round;
          void T::load(Round&);
          Round::WH T::fire();
      }
      concept_map tanks<Tank_KE_90>{}
      concept_map tanks<Tank_HE_90>{}
      concept_map tanks<Tank_KE_120>{}
      concept_map tanks<Tank_HE_120>{}
      //坦克构造函数模板
      template<tanks T>
      T* CreateTank(WHType type, int level) { //WHType是一个枚举表明炮弹种类
          switch(level)
          {
          case 1:
              if(type==wht_KE)
                  return new tanks<Tank_KE_90>;
              else
                  return new tanks<Tank_HE_90>;
          case 2:
              if(type==wht_KE)
                  return new tanks<Tank_KE_120>;
              else
                  return new tanks<Tank_HE_120>;
          default:
              throw error("no such tank.");
          }
      }
      //玩家类
      class player
      {
      public:
          void update() {
m_pTank=CreateTank(m_tankType, ++m_level);
          }
          ...
      private:
          int m_level;
          WHType m_tankType;
tanks* m_pTank;
      };
    在类player中,使用了一个concept,而不是一个类型,来定义一个对象。根据前面提到的concept推导规则,m_pTank指向一个动态对象,还是静态对象,取决于为它赋值的表达式类型是concept还是具体类型。在update()函数中,可以看到,m_pTank通过表达式CreateTank(m_tankType, ++m_level)赋值。那么这个函数的返回类型,将决定m_pTank的类型。CreateTank()是一个模板,返回类型是模板参数,并且是符合concept tanks的类型。关键在于代码中的return new tanks<...>语句。前文已经说过,这种形式是使用<...>中的类型创建一个符合tanks的动态对象。所以,CreateTank()返回的是动态对象。那么,m_pTank也将指向一个动态对象。在运行时,当玩家达到一定条件,便可以升级。update()成员函数将根据玩家的级别重新创建相应的坦克对象,赋值到m_pTank中。
    这里,实际上是利用tanks这个concept描述,充当类型的公有接口。它所具有的特性同动多态的抽象基类是非常相似的。但是所不同的是,如同我在代码中展现的那样,concept作为接口,可以在任何时候定义,同类型绑定。而无需象抽象基类那样,必须在类型定义之前定义。于是,这种非侵入式的接口相比抽象基类拥有更加灵活自由的特性。
    然而,事情还没有完。在进一步深化坦克案例后,我们还将发现runtime GP拥有更加有趣和重要的特性。
    坦克开炮为的是攻击目标。对目标的毁伤情况直接关系到玩家的生存和得分。所以,我们必须对射击后,目标的损毁情况进行计算。于是编写了这样一组函数:
      double LethalityEvaluate(Target& t, double hitRate, WH_KE_90& wh) {...}
      double LethalityEvaluate(Target& t, double hitRate, WH_HE_90& wh) {...}
      double LethalityEvaluate(Target& t, double hitRate, WH_KE_120& wh) {...}
      double LethalityEvaluate(Target& t, double hitRate, WH_HE_120& wh) {...}
    Target是目标;hitRate是命中率,根据坦克和目标的位置、射击参数综合计算获得(如果想要更真实,可以加上风向、风力、气温、湿度、海拔等等因素);wh就是发射出来的炮弹了。函数返回杀伤率。如此,我们便可以在射击之后进行评估了:
      double l=LethalityEvaluate(house, hr, myTank.fire());
     现在,游戏需要进行一些扩展,增加一个升级,允许坦克升级到第三级。到了第三极,主炮的口径就升到头了,但可以升级功能,可以发射穿甲弹和高爆弹。这样,我们就需要一个“两用”的主炮类。但是,实际上并不需要直接做这么一个类,只需要用“两用”的弹药来实例化Cannon模板:
      concept Warheads120<T> : Warheads<T> { //120mm炮弹头concept
          double LethalityEvaluate(Target& t, double hitRate, T& wh);
      }
      concept Rounds120<T> : Rounds<T> {}
      concept_map Warheads120<WH_KE_120> {}  //120mm的穿甲弹属于Warheads120
      concept_map Warheads120<WH_HE_120> {}  //120mm的高爆弹属于Warheads120
      template<WH120 WH> concept_map Rounds120<Round<WH>> {} //所有弹头是Warheads120的炮弹都是属于Rounds120
      typedef Canon<Rounds120> Cannon120; //用Rounds120实例化Cannon模板,得到“两用”主炮
    一堆炫目的concept和concept_map之后,得到Rounds120,就是所谓的“两用”弹药。作为一个concept,它同两种类型map 在一起,实际上就成了这两个类型的接口。当我们使用Rounds120实例化Cannon<>模板时,也就创建了一个“两用的主炮”(使用 Rounds120弹药的主炮)。如果用这个Cannon120实例化Tank模板,那么就可以得到第三级坦克(装上Cannon120主炮的坦克就是第三级):
      typedef Tank<Cannon120> TankL3;
    于是,我们可以使用不同的120mm弹药装填主炮,并且发射相应的炮弹:
      TankL3 tank_l3;
      Round_KE_120 ke_round;  //创建一枚穿甲弹
      Round_HE_120 he_round;  //创建一枚高爆弹
      tank_l3.load(ke_round);  //装填穿甲弹
      tank_l3.fire();               //发射穿甲弹
      tank_l3.load(he_round);  //装填高爆弹
      tank_l3.fire();               //发射高爆弹
    现在,我们把注意力从消灭敌人,转移到TankL3::load()的参数类型和TankL3::fire()的返回类型上。在一级和二级坦克(类型 Tank_KE_90等)上,load()成员的参数类型是Round_KE_90等具体的类型;而fire()的返回类型亦是如此。但TankL3是用 Cannon120实例化的,而Cannon120是用Rounds120这个concept实例化的。根据Tank<>模板的定义, load()成员的参数类型实际上是模板参数上的一个associate type。而这个associate type实际上就是Rounds120。这意味着load()实例化后的签名是:void load(Rounds120& r)(这里暂且允许concept作为类型参数使用)。只要符合Rounds120的类型都可以作为实参传递给load()成员。同样,fire()成员的返回类型来自于Round120上的associate type,也是个concept。因此,fire()实例化后的签名是:Warheads120 fire()。
    接下来值得注意的是fire()成员。它返回类型是一个concept,那么返回的将是一个动态对象。在运行时,它可能返回WH_KE_120的实例,也可能返回WH_HE_120的实例,取决于运行时load()函数所装填的炮弹类型。当我们采用LethalityEvaluate()函数对该炮弹的杀伤情况进行评估将会出现比较微妙的情况:
      double x=LethalityEvaluate(hisTank, hr, tank_l3.fire());
    这时候,编译器应当选择哪个LethalityEvaluate()?由于tank_l3.fire()返回的是一个动态对象,具体的类型编译时不知道。实际上,在正宗的静态语言中,这样的调用根本无法通过编译。当然,编译器可以通过runtime reflect获得类型信息,然后在LethalityEvaluate()的重载中匹配正确的函数。然而,这种动态语言做法会造成性能上的问题,为静态语言所不屑。
    但是,在这里,在runtime concept的作用下,我们可以使这种调用成为静态的、合法的,并且是高效的。请注意我在concept Warhead120的定义中加入了一个associate function:double LethalityEvaluate(Target& t, double hitRate, T& wh);。runtime concept会很忠实地将concept定义中的associate function构造到一个函数指针表(我称之为ctable)中。(详细情况请看本文附录和这篇文章的附录)。因此,与tank_l3.fire()返回的动态对象实际类型对应的LethalityEvaluate()函数版本的指针正老老实实地躺在相应的ctable里。所以,我们可以直接从动态对象上获得指向ctable的指针,并且找出相应的LethalityEvaluate()函数指针,然后直接调用即可。比如:
      tank_l3.load(ke_round);
      double x=LethalityEvaluate(hisTank, hr, tank_l3.fire());
    在这些代码的背后,ke_round通过load()装填入主炮后,便摇身变成了一个动态对象。编译器会为它附加上指向ctable的指针,然后在调用 fire()的时候返回指向这个动态对象的引用。此时,编译器发现这个动态对象所对应的Warhead120 concept上已经定义了一个名为LethalityEvaluate()的associate function,并且签名与当前调用相符。于是,便可以直接找到ctable中LethalityEvaluate()对应的那个函数指针,无所顾忌的调用。由于一个concept的associate function肯定是同实际类型匹配的函数版本。比如,对于WH_HE_120而言,它的associate function LethalityEvaluate()是版本:double LethalityEvaluate(Target& t, double hitRate, WH_HE_120& wh) {...}。其他版本的LethalityEvaluate()都无法满足concept Warhead120施加在类型WH_HE_120上的约束。
    这个特性就使得runtime concept作为接口,相比动多态的抽象接口,具有更大的灵活性。抽象接口只表达了类的成员,以及类本身的行为,无法表达类型同其他类型的互动关系,或者说类型间的交互。而concept同时描述了成员函数和相关的自由函数(包括操作符),使得类型间的关系也可以通过接口直接获得,无需再通过 reflect等间接的动态手段。
    这一点在处理内置类型(如int、float)、预置类型(某些库中的类型)、第三方类型等不易或无法修改的类型有至关重要的作用。在OOP下,我们无法输出一个“整数”,或许是short、或许是long,甚至是unsinged longlong。为此,我们要么把它们转换成一个“最基础类型”(C/C++的void*,或C#的Object*),然后运用rtti信息进行类型转换,再做处理;要么使用variant这种类型包装(就像COM中的那样),然后为他们全面定义一套计算库。但runtime concept不仅仅允许输出“整数”这样一个动态对象,而且还将相关的各种操作附在动态对象之上,使之无需借助rtti或者辅助类型也可进行各类处理,就如同处理具体类型的对象那样。
    但是,在这里我仅仅考察了针对一个类型的concept(暂且称之为一元concept),还未涉及两个和两个以上类型的concept(暂且称为多元 concept,或n-元concept)。在实际开发中,多数操作都会涉及多个对象,比如两个数相加、一种类型转换成另一种。此时,我们将会面对多元的 concept。但是多元的runtime concept的特性还不清楚,还需要进一步研究分析。

总结

    本文初步展示了在引入runtime concept之后,GP的动态化特性。归纳起来有以下几点:
  1. static GP和runtime GP之间在形式上完全统一,两者可以看作同一种抽象机制的不同表现。因此,我们在构造类型、函数等代码实体的时候,并不需要考虑它们将来需要作为static使用,还是runtime使用。static和runtime的控制完全取决于这些代码实体的使用方式。这就很好地减少了软件项目早期设计,以及库设计的前瞻性方面压力。
  2. runtime concept作为非侵入式的接口,可以非常灵活地使用。我们无需在代码编写的一开始就精确地定义好接口,可以先直接编写功能类型,逐步构建软件结构。需要时再定义接口(concept),并可以在任何时候与类型绑定。接口的定制可以成为一个逐步推进的过程,早期的接口设计不足产生的不良影响相应地弱化了。
  3. runtime concept相比动多态的抽象接口更加自由。concept可以对类型的成员函数、自由函数、类型特征等等方面的特性作出描述。在runtime化之后,相关自由函数成为了接口的一部分。更进一步规约了类型在整体软件的代码环境中的行为特征。同时,也为动态对象的访问提供更多的信息和手段。
  4. concept不仅实现类型描述,还可以进一步描述类型之间的关系。这大大完善了抽象体系。特别在runtime情况下,这种更宽泛的类型描述能力可以起到两个作用:其一,进一步约束了动态对象的行为;其二,为外界操作和使用类型提供更多的信息,消除或减少了类型匹配方面的抽象惩罚。这个方面的更多特性尚不清楚,还需要更进一步地深入研究。
    综上所述,我们可以看到runtime GP在不损失性能的情况下,具备相比动多态更灵活、更丰富的手段。从根本上而言,以concept为核心的GP提供了更基础的抽象体系(关于这方面探讨,请看我的这篇文章中关于concept对类型划分的作用部分)。或者说,concept的类型描述和约束作用体现了类型抽象的本质,而在此基础上进一步衍生出static和runtime两种具体的使用方式。这也就是所谓:道生一,一生二。:)

附录

Runtime Concept实现方案二

    我在这篇文章附录里,给出了一种实现runtime concept的可能方案。这里,我进一步对这个方案做了一些改进,使其更加精简、高效。
    假设我们有一个concept:
    concept Shape<T>
    {
        void T::load(xml);
        void T::draw(device);
        void move(T&);
    }
    另外,还有一个代表圆的concept:
    concept Cycles<T> :
        CopyConstructable<T>,
        Assignable<T>,
        Swappable<T>,
        Shape<T>
    {
        T::T(double, double, double);
        double T::getX();
        double T::getY();
        double T::getR();
        void T::setX(double);
        void T::setY(double);
        void T::setR(double);
    }
    现在有类型Cycle:
    class Cycle
    {
    public:
        Cycle(double x, double y, double r);
        Cycle(Cycle const& c);
        Cycle& operator=(Cycle const& c);
        void swap(Cycle const& c);
        void load(xml init);
        void draw(device dev);
        double getX();
        double getY();
        double getR();
        void setX(double x);
        void setY(double y);
        void setR(double r);
    private:
        ...
    };
    我们将类型Cycle map到concept Cycles上:
      concept_map Cycles<Cycle>{}
    当我们创建对象时,将会得到如下图的结构:

runtime concept-2

    concept表(concept list)不再同对象放在一起,而是同一个类型的类型信息放在一起。一同放置的还有ctable。ctable中每个对应的concept项都有一个指向 concept表的指针(也可以指向类型信息头),用以找到concept list,执行concept cast。动态对象,或动态对象的引用/指针上只需附加一个指向相应的concept的指针即可。相比前一个方案,内存利用率更高。

posted @ 2008-01-06 17:17 longshanks 阅读(1265) | 评论 (6)编辑 收藏

2007年12月17日 #

被误解的C++——汉尼拔

by 莫华枫

    公元前216年8月2日,意大利东部平原,一个叫做坎尼的地方,两支大军摆开阵势,准备决一死战。一方是由保罗斯和瓦罗两位执政官率领的罗马人,另一方则是伟大的军事天才汉尼拔*巴卡率领的迦太基军队及其同盟。罗马人超过8万,而迦太基仅有4万余人。然而到了傍晚,罗马人被彻底击败,7万人被杀,仅有少数得以逃脱。这就是著名的坎尼会战。经此一役,(外加先前进行的特利比亚和特拉西梅诺湖会战),罗马人元气大伤,成年公民损失达五分之一。部分城邦背叛罗马,西西里也发生起义。罗马已经到了摇摇欲坠的地步。

    汉尼拔的这些胜利,完全得益于先前的一次异乎寻常的远征。公元前218年,汉尼拔率领军队,从新迦太基城(西班牙)出发,翻越比利牛斯山,进入南高卢地域。在他面前有两条路可走,翻越阿尔俾斯山,或者沿海岸进入意大利。但是,当时罗马人已在沿海地区部署了两支部队,准备拦截汉尼拔。而且,罗马人的海军优势,使得他们可以在任何时候将一支部队登陆在他的背后。而翻越阿尔俾斯山,则是一条及其艰险的道路,更何况是在冬天。

    汉尼拔选择了阿尔俾斯山。他甩开了罗马人,从小圣贝纳德和日内瓦山之间越过阿尔俾斯山,进入意大利境内。此时,罗马人便失去了战略纵深,一把尖刀已经深深地插入他们的腹内...

 

    C++的发展史上,也有着如同汉尼拔翻越阿尔俾斯山远征。一切还得从C with Class时代说起。

    Bjarne曾经反复强调,他创建C++为的是将Simular的抽象能力同C的性能结合起来。于是,在C语言的基础上,诞生了一种拥有类、继承、重载等等面向对象机制的语言。在这个阶段,C++提供了两个方面的抽象能力。一种是数据抽象,也就是将数据所要表达的含义通过类型以及依附于类型上的成员表述。另一种则是多态,一种最原始的多态(重载)。

    数据抽象,通过被称为“抽象数据类型(ADT)”的技术实现。ADT的一种方案,就是类。类所提供的封装性将一个数据实体的外在特征,或者说语义的表述形式,同具体的实现,比如数据存储形式,分离。这样所增加的中间层将数据的使用者同数据的实现者隔离,使得他们使用共同的约定语义工作,不再相互了解彼此的细节,从而使得两者得以解耦。

    多态则是更加基础更加重要的一种特性。多态使得我们得以用同一种符号实现某种确定的语义。多态的精髓在于:以一种形式表达一种语义。在此之前,我们往往被迫使用不同的符号来代表同一种抽象语义,为的是适应强类型系统所施加的约束。比如:

//代码#1
int add_int(int lhs, int rhs);
float add_float(float lhs, float rhs);

    很显然,这两个函数表达的语义分别是“把两个int类型值加在一起”和“把两个float类型值加在一起”。这两个语义抽象起来都表达了一个意思:加。

    我们在做算术题的时候是不会管被计算的数字是整数还是实数。同样,如果能够在编程的时候,不考虑算术操作对象的类型,只需关心谁和谁进行什么操作,那么会方便得多。当C++引入重载后,这种愿望便得以实现:

//代码#2
int add(int lhs, int rhs);
float add(float lhs, float rhs);

    重载使得我们只需关心“加”这个语义,至于什么类型和什么类型相加,则由编译器根据操作数的类型自动解析。

    从某种意义上说,重载是被长期忽视,但却极为重要的一个语言特性。在多数介绍OOP的书籍中,重载往往被作为OOP的附属品,放在一些不起眼的地方。它的多态本质也被动多态的人造光环所设遮蔽。然而,重载的重要作用却在实践中潜移默化地体现出来。重载差不多可以看作语言迈入现代抽象体系的第一步。它的实际效用甚至要超过被广为关注的OOP,而不会像OOP那样在获得抽象的同时,伴随着不小的副作用。

     随着虚函数的引入,C++开始具备了颇具争议的动多态技术。虚函数是一种依附于类(OOP的类型基础)的多态技术。其技术基础是后期绑定(late-binding)。当一个类D继承自类B时,它有两种方法覆盖(override)B上的某个函数:

//代码#3
class B
{
public:
    void fun1();
    virtual void fun2();
};

class D:public B
{
public:
    void fun1();
    void fun2();
};

    当继承类D中声明了同基类B中成员函数相同函数名、相同签名的成员函数,那么基类的成员函数将被覆盖。对于基类的非虚成员函数,继承类会直接将其遮蔽。对于类型D的使用者,fun1代表了D中所赋予的语义。而类型D的实例,可以隐式地转换成类型B的引用b,此时调用b的fun1,则执行的是类B的fun1 定义,而非类D的fun1,尽管此时b实际指向一个D的实例。

    但是,如果继承类覆盖了基类的虚函数,那么将得到相反的结果:当调用引用b的fun2,实际上却是调用了D的fun2定义。这表明,覆盖一个虚函数,将会在继承类和基类之间的所有层次上执行覆盖。这种彻底的、全方位的覆盖行为,使得我们可以在继承类上修饰或扩展基类的功能或行为。这便是OOP扩展机制的基础。而这种技术被称为动多态,意思是基类引用所表达的语义并非取决于基类本身,而是来源于它所指向的实际对象,因此它是“多态”的。因为一个引用所指向的对象可以在运行时变换,所以它是“动”的。

    随着动多态而来的一个“副产品”,却事实上成为了OOP的核心和支柱。虚函数的“动多态”特性将我们引向一个极端的情况:一个都是虚函数的类。更重要的,这个类上的虚函数都没有实现,每个虚函数都未曾指向一个实实在在的函数体。当然,这样的类是无法直接使用的。有趣的是,这种被称为“抽象基类”的类,迫使我们继承它,并“替它”实现那些没有实现的虚函数。这样,对于一个抽象基类的引用,多态地拥有了继承类的行为。而反过来,抽象基类实际上起到了强迫继承类实现某些特定功能的作用。因此,抽象基类扮演了接口的角色。接口具有两重作用:一、约束继承类(实现者)迫使其实现预定的成员函数(功能和行为);二、描述了继承类必定拥有的成员函数(功能和行为)。这两种作用促使接口成为了OOP设计体系的支柱。

    C++在这方面的进步,使其成为一个真正意义上具备现代抽象能力的语言。然而,这种进步并非“翻越阿尔俾斯山”。充其量也只能算作“翻越比利牛斯山”。对于C++而言,真正艰苦的远征才刚开始,那令人生畏的“阿尔俾斯山”仍在遥远的前方。

    同汉尼拔一样,当C++一脚迈入“现代抽象语言俱乐部”后,便面临两种选择。或者在原有基础上修修补补,成为一种OOP语言;或者继续前进,翻越那座险峻的山峰。C++的汉尼拔——Bjarne Stroustrup——选择了后者。

    从D&E的描述中我们可以看到,在C++的原始设计中就已经考虑“类型参数”的问题。但直到90年代初,才真正意义上地实现了模板。然而,模板只是第一步。诸如Ada等语言中都有类似的机制(泛型,generic),但并未对当时的编程技术产生根本性的影响。

    关键性的成果来源于Alex Stepanov的贡献。Stepanov在后来被称为stl的算法-容器库上所做的努力,使得一种新兴的编程技术——泛型编程(Generic Programming,GP)——进入了人们的视野。stl的产生对C++的模板机制产生了极其重要的影响,促使了模板特化的诞生。模板特化表面上是模板的辅助特性,但是实际上它却是比“类型参数”更加本质的机能。

    假设我们有一组函数执行比较两个对象大小的操作:

//代码#4
int compare(int lhs, int rhs);
int compare(float lhs, float rhs);
int compare(string lhs, string rhs);

    重载使得我们可以仅用compare一个函数名执行不同类型的比较操作。但是这些函数具有一样的实现代码。模板的引入,使得我们可以消除这种重复代码:

//代码#5
template<typename T> int compare(T lhs, T rhs) {
    if(lhs==rhs)
        return 0;
    if(lhs>rhs)
        return 1;
    if(lhs<rhs)
        return -1;
}

    这样一个模板可以应用于任何类型,不但用一个符号表达了一个语义,而且用一个实现代替了诸多重复代码。这便是GP的基本作用。

    接下来的变化,可以算作真正意义上的“登山”了。

    如果有两个指针,分别指向两个相同类型的对象。此时如果我们采用上述compare函数模板,那么将无法得到所需的结果。因为此时比较的是两个指针的值,而不是所指向的对象本身。为了应付这种特殊情况,我们需要对compare做“特别处理”:

//代码#6
template<typename T> int compare(T* lhs, T* rhs) {
    if(*lhs==*rhs)
        return 0;
    if(*lhs>*rhs)
        return 1;
    if(*lhs<*rhs)
        return -1;
}

    这个“特殊版本”的compare,对于任何类型的指针作出响应。如果调用时的实参是一个指针,那么这个“指针版”的compare将会得到优先匹配。如果我们将compare改成下面的实现,那么就会出现非常有趣的行为:

//代码#7
template<typename T>
struct comp_impl
{
    int operator()(T lhs, T rhs) {
        if(lhs==rhs)
            return 0;
        if(lhs>rhs)
            return 1;
        if(lhs<rhs)
            return -1;
    }
};
template<typename T>
struct comp_impl<T*>
{
    int operator()(T* lhs, T* rhs) {
        comp_impl<T>()(*lhs, *rhs);
    }
};
template<typename T> int compare(T* lhs, T* rhs) {
    comp_impl<T>()(*lhs, *rhs);
}

    当我们将指针的指针作为实参,调用compare时,神奇的事情发生了:

//代码#8
double **x, **y;
compare(x, y);

    compare居然成功地剥离了两个指针,并且正确地比较了两个对象的值。这个戏法充分利用了类模板的局部特化和特化解析规则。根据规则,越是特化的模板,越是优先匹配。T*版的comp_impl比T版的更加“特化”,会得到优先匹配。那么当一个指针的指针实例化comp_impl,则会匹配T*版的 comp_impl,因为指针的指针,也是指针。T*版通过局部特化机制,剥离掉一级指针,然后用所得的类型实例化comp_impl。指针的指针剥离掉一级指针,那么还是一个指针,又会匹配T*版。T*版又会剥离掉一级指针,剩下的就是真正可以比较的类型——double。此时,double已无法与 T*版本匹配,只能匹配基础模板,执行真正的比较操作。

    这种奇妙的手法是蕴含在模板特化中一些更加本质的机制的结果。这种意外获得的“模板衍生产品”可以算作一种编译时计算的能力,后来被一些“好事者”发展成独立的“模板元编程”(Template Meta Programming,TMP)。

    尽管TMP新奇而又奥妙,但终究只是一种辅助技术,用来弥补C++的一些缺陷、做一些扩展,“捡个漏”什么的。不过它为我们带来了两点重要的启示:一、我们有可能通过语言本身的一些机制,进行元编程;二、元编程在一定程度上可以同通用语言一起使用。这些启示对编程语言的发展有很好的指导意义。

    模板及特化规则是C++ GP的核心所在。这些语言特性的强大能力并非凭空而来。实际上有一只“幕后大手”在冥冥之中操纵着一切。

    假设有一个类型系统,包含n个类型:t1,...,tn,那么这些类型构成了一个集合T={t1,...,tn}。在当我们运用重载技术时,实际上构造了一组类型的tuple到函数实现的映射:<ti1,ti2,ti3,...> ->fj()。编译器在重载解析的时候,就是按照这组映射寻找匹配的函数版本。当我们编写了形如代码#5的模板,那么就相当于构建了映射:<T, T,...> ->f0()。

    而代码#6,以及代码#7中的T*版模板,实际上是构造了一个<Tp>->fp()的映射。这里Tp是T的一个子集:Tp={t'|t'=ti*, ti∈T}。换句话说,特化使泛型体系细化了。利用模板特化技术,我们已经能够(笨拙地)分辨浮点数、整数、内置类型、内置数组、类、枚举等等类型。具备为类型划分的能力,也就是构造不同的类型子集的能力。

    现在,我们便可以构造一个“泛型体系”:G={T} U T U Tp U Ta U Ti U Tf U Tc ...。其中,Tp是所有指针类型,Ta是数组,Ti是整数,Tf是浮点数,Tc是类等等。但是如果我们按照泛化程度,把G中的元素排列开:{T, Tp, Ta, Ti,...,t1,...,tn}。我们会发现这中间存在一些“断层”。这些断层位于T和Tp等之间,以及Tp等与ti等之间等等。这表明在C++98/03中,抽象体系不够完整,存在缺陷。

    所以,到目前为止,C++还没有真正翻越阿尔俾斯山里那座最险峻的山峰。这正是C++0x正在努力做的,而且胜利在望。

    在C++0x中,大牛们引入了first-class的concept支持。concept目前还没有正式的法定描述(以及合理的中文翻译)。通俗地讲,concept描述了一个类型的(接口)特征。说具体的,一个concept描述了类型必须具备的公共成员函数,必须具备的施加在该类型上的自由函数(和操作符),以及必须具备的其他特征(通过间接手段)。下面是一个典型的concept:

concept has_equal<T>
{
    bool T::equal(T const& v);
};

    这个concept告诉我们它所描述的类型必须有一个equal成员,以另一个同类型的对象为参数。当我们将这个concept施加在一个函数模板上,并作为对类型参数的约束,那么就表明了这个模板对类型参数的要求:

template<has_equal T>bool is_equal(T& lhs, T const& rhs) {
    return lhs.equal(rhs);
}

    如果实参对象的类型没有equal成员,那么is_equal将会拒绝编译通过:这不是我要的!

    concept是可以组合的,正式的术语叫做“refine”。我们可以通过refine进一步构造出约束更强的concept:

concept my_concept<T> : has_equal<T>, DefaultConstructable<T>, Swappable<T> {}

    refine获得的concept将会“继承”那些“基concept”的所有约束。作为更细致的组合手段,concept还可以通过!操作符“去掉”某些内涵的concept约束:

concept my_concept1<T> : has_equal<T>, !DefaultConstructable<T> {}

    这个concept要求类型具备equal成员,但不能有默认构造函数。

    通过这些手段,concept可以“无限细分”类型集合T。理论上,我们可以制造一连串只相差一个函数或者只相差一个参数的concept。

    一个concept实际上就是构成类型集合T的划分的约束:Tx={ti| Cx(ti)==true, ti∈T}。其中Cx就是concept所构造的约束。不同的concept有着不同范围的约束。这样,理论上我们可以运用concept枚举出类型集合 T的所有子集。而这些子集则正好填补了上述G中的那些断层。换句话说,concept细化了类型划分的粒度,或者说泛型的粒度。使得“离散”的泛型系统变成“连续”的。

    当我们运用concept约束一个函数模板的类型参数时,相当于用concept所描述的类型子集构建一个映射:<Tx1,Tx2,...>->fx()。凡是符合tuple <Tx1,Tx2,...>的类型组合,对应fx()。所以,从这个角度而言,函数模板的特化(包括concept)可以看作函数重载的一种扩展。在concept的促进下,我们便可以把函数模板特化和函数重载统一在一个体系下处理,使用共同的规则解析。

    在目前阶段,C++差不多已经登上了“抽象阿尔俾斯山”的顶峰。但是就如同汉尼拔进入意大利后,还需要面对强盛的罗马共和国,与之作战那样。C++的面前还需要进一步将优势化作胜利。要做的事还很多,其中最重要的,当属构建Runtime GP。目前C++的GP是编译时机制。对于运行时决断的任务,还需要求助于OOP的动多态。但是C++领域的大牛们已经着手在Runtime GP和Runtime Concept等方面展开努力。这方面的最新成果可以看这里这里这里。相信经过若干年的努力后,GP将会完全的成熟,成为真正主流的编程技术。

 

    坎尼会战之后,汉尼拔已经拥有了绝对的优势。罗马人已经战败,他同罗马城之间已经没有任何强大的敌对力量,罗马人也已经闻风丧胆,几无斗志。但是,汉尼拔却犯下了或许令他一生后悔的错误。他放过了罗马城,转而攻击罗马的南部城邦和同盟。他低估了罗马人的意志,以及罗马同盟的牢固程度。罗马人很快结束了最初的混乱,任命了新的执政官,采用了坚壁清野、以柔克刚的新战略。随着时间的推移,汉尼拔和他的军队限于孤立无援的境地,被迫为了生存而作战。尽管迫降并占领了几个罗马城市,但是终究无法再次获得给予罗马人致命一击的机会。

    汉尼拔的战略错误实际上在从新迦太基城出发之前已经注定。因为汉尼拔对罗马人的远征的根本目的并非击溃并占领罗马,而是通过打击罗马,削弱他们的势力,瓦解他们的联盟。以达到寻求签订和平协议的目的。然而这种有限战略却使导致了他的最终失败。

 

    不幸的是,C++或多或少地有着同汉尼拔一样的战略错误。C++最初的目的基本上仅仅局限于“更好的C”。并且全面兼容C。这在当时似乎很合理,因为C可以算作最成功的“底层高级语言”,拥有很高的性能和灵活性。但是,C的设计并未考虑将来会有一个叫做“C++”的语言来对其进行扩展。结果很多优点对于C而言是优点,但却成了C++的负担。比如,C大量使用操作符表达语法结构,对于C而言显得非常简洁,但对于C++,则使其被迫大规模复用操作符,为其后出现的很多语法缺陷埋下了伏笔。这一点上,Ada做得相对成熟些。它从Pascal那里继承了主要语法,但不考虑兼容。这使得Ada更加完整,易于发展。新语言就是新语言,过分的兼容是镣铐,不是优势。而且,合理地继承语法,同样可以吸引众多开发者。从经验来看,程序员对于语法变化的承受能力还是很强的。他们更多地关心语言的功能和易用性。

    另一方面,C++最初并未把目标定在“创建一种高度抽象,又确保性能的语言”。纵观C++的发展,各种抽象机制并非在完整的规划或路线图的指导下加入语言。所有高级特性都是以“添油战术”零打碎敲地加入语言。从某种程度上来看,C++更像是一种实验性语言,而非工业语言。C++的强大功能和优点是长期积累获得的,而它的诸多缺陷也是长期添加特性的结果。

    汉尼拔和C++给予我们一个很好的教训。对于一个试图在1、20年后依然健康成长的语言,那么就必须在最初便拥有明确的目标和技术发展规划。对于以往的语言特性应当择优而取,不能照单全收。并且在技术上拥有足够的前瞻性。我们知道,技术前瞻性是很难做到的,毕竟技术发展太快。如果做不到,那就得有足够的魄力对过去的东西加以取舍。所谓“舍小就大,弃子争先”。

    总体而言,C++在抽象机制的发展方面,还算是成功的。尽管伴随着不少技术缺陷,但是C++的抽象能力在各种语言中可称得上出类拔萃。而且C++还在发展,它未来将会发展成什么形态,不得而知。但是,无论C++是继续修修补补,还是根本性地变革,它的抽象能力都会不折不扣地保留,并且不断完善和增强。

 

    坎尼会战之后,汉尼拔又打过几次小规模的胜仗。但经过长期的作战,也得不到迦太基的支援,汉尼拔的力量越来越弱,只能在意大利半岛上勉强生存。罗马很快恢复了元气,改革了军事体系和作战方式,重新掌握了战略主动权。更重要的是,罗马也有了自己的“汉尼拔”——(征服非洲的)普布利乌斯·科尔内利乌斯·西庇阿(大西庇阿)。西庇阿被派往北非大陆,直接攻击迦太基人的老巢。汉尼拔被召回,在扎马与西庇阿摆开阵势,展开一场决战。最终,西庇阿运用从汉尼拔那里学到的战术击溃了迦太基人,为罗马人赢得了第二次布匿战争的胜利。

    此后,汉尼拔在罗马人的通缉之下,流亡于地中海沿岸,试图寻求东山再起的机会。但最终未能如愿,被迫于公元前183年自尽,享年64岁。有趣的是,他的老对手,小他12岁的西庇阿也于同年去世。一个伟大的传奇就此结束。

posted @ 2007-12-17 11:28 longshanks 阅读(1098) | 评论 (5)编辑 收藏

2007年12月6日 #

    本文来源于TopLanguage Group 上的一次讨论(这里这里这里 )。pongba提出:C++的抽象机制并不完善,原因是为了性能而做的折中,未来随着计算能力的提高到一定程度,人们就能够忽略更好的抽象所带来的负面效应。就此诸老大各自提出高见,受益良多啊。经过讨论,我基本上理解了pongba的想法。但我觉得等待计算机的性能提高太消极了。我相信随着编程技术的发展,这种最优抽象造成的性能损失将会越来越小。这种途径将会更快地让人们接受最优抽象形式。

     在“C++ Template”一书中,将多态总结为三种主要类型:runtime bound、static unbound和runtime unbound。其中runtime bound就是我们通常所说的动多态,OOP的核心支柱(广义上OOP还包括Object Base(OB,仅指类型封装等OO的基本特性),但有时也会将OB和OOP分开,OOP单指以OO为基础的动多态。这里使用狭义的OOP含义); static unbound就是静多态,通过模板实现。而runtime unbound则是一种不常见的形式。早年的SmallTalk具有这种形式,现在的ruby也引入这种机制。
     在主流的(静态)语言中,我们会面临两种类型的多态需求:对于编译期可以确定类型的,使用静多态,比如实例化一个容器;对于运行期方能确定类型的,则使用 动多态。而runtime unbound也可以用于运行期类型决断。于是,便有了两种运行期多态。这两种多态的特性和他们的差异,是本文的核心。实际上,相比动多态, runtime unbound多态为我们提供了更本质的运行时多态手段,我们可以从中获得更大的收益。但是鉴于一些技术上的困难,runtime unbound多态无法进入主流世界。不过,由于新的编程技术的出现,使得这种更好的运行时多态形式可以同动多态一比高下。

动多态   

    废话少说,让我们从一个老掉牙的案例开始吧:编写一个绘图程序,图形包括矩形、椭圆、三角形、多边形等等。图形从脚本(比如xml)中读出,创建后保存在一个容器中备查。通过遍历容器执行图形绘制。
    就这么个题目,很简单,也很熟悉,解释OOP的动多态最常用的案例。下面我们就从动多态实现开始。
    首先定义一个抽象基类,也就是接口:

    class IShape

    {

        virtual void load(xml init)=0;

        virtual void draw(monitor m)=0;

        ...

    };

    然后定义各种图形类,并从这个接口上继承:

    class Rectangle: public IShape

    {

        void load(xml init) {...}

        void draw(monitor m) {...}

        ...

    };

    class Ellipse: public IShape

    {

        void load(xml init) {...}

        void draw(monitor m) {...}

        ...

    };

    ...

 

    void DrawShapes(monitor m, vector<IShape*> const& g)

    {

        vector<IShape*>::const_iterator b(g.begin()), e(g.end());

        for(; b!=e; ++b)

        {

            (*b)->draw(m);

        }

    }

    ...

    现在可以使用这些图形类了:

    vector<IShape*> vg;

    vg.push_back(new Rectangle);

    vg.push_back(new Ellipse);

    ...

    DrawShapes(crt, vg);

    通过接口IShape,我们可以把不同的图形类统一到一种类型下。但是,通过虚函数的override,由图形类实现IShape上的虚函数。这可以算老 生常谈了。动多态的核心就是利用override和late bound的组合,使得一个基类可以在类型归一化的情况下,拥有继承类的语义。OOP设计模式大量运用这种技术,实现很多需要灵活扩展的系统。

Runtime Unbound

    Runtime Unbound多态混合了静多态和动多态的特征,即既有类型泛化,又是运行时决断的。一个最典型的例子就是ruby的函数:
    class x
       def fun(car)
            car.aboard
        end
    end
    这个案例非常明确地展示出了Runtime Unbound多态的特点。car参数没有类型,这里也不需要关心类型,只要求car对象有一个aboard方法即可。由于ruby是动态语言,能够运行时检测对象的特征,并动态调用对象上的方法。
    在Runtime Unbound的思想指导下,我们利用一种伪造的“动态C++”,把上面的绘图例子重新编写:

    class Rectangle

    {

        void load(xml init) {...}

        void draw(monitor dev) {...}

        ...

    };

    class Ellipse

    {

        void load(xml init) {...}

        void draw(monitor dev) {...}

        ...

    };

    ...

    void DrawShapes(monitor dev, vector<anything> const& g)

    {

        vector<IShape>::const_iterator b(g.begin()), e(g.end());

        for(; b!=e; ++b)

        {

            (*b).draw(dev);

        }

    }

    ...

    vector<anything> vg;

    vg.push_back(Rectangle(...));

    vg.push_back(Ellipse(...));

    ...

    DrawShapes(crt, vg);

    图形类不再从抽象接口IShape继承,而用关键字anything实例化vector<>模板。这个虚构的anything关键字所起的作 用就是使得vector能够接受不同类型的对象。当DrawShapes()函数接收到存放图形对象的容器后,遍历每一个对象,并且调用对象上的draw ()函数,而不管其类型。
    从这段代码中,我们可以看出Runtime Unbound多态带来的好处。所有图形类不再需要归一化成一个类型(抽象接口)。每个类只需按照约定,实现load、draw等成员函数即可。也就是 说,这些图形类解耦合了。一旦类型解耦,便赋予我们很大的自由度。最典型的情况就是,我们需要使用一个其他人开发的图形类,并且无法修改其实现。此时,如 果使用动多态,就很麻烦。因为尽管这些图形类都拥有load、draw等函数,但毕竟不是继承自IShape,无法直接插入容器。必须编写一个继承自 IShape的适配器,作为外来图形类的包装,转发对其的访问。表面上,我们只是减少一个接口的定义,但Runtime Unbound多态带来的解耦有着非凡的意义。因为类耦合始终是OOP设计中的一个令人头痛的问题。在后面,我们还将看到建立在Runtime Unbound多态基础上的更大的进步。
    然而,尽管Runtime Unbound多态具有这些优点,但因为建立在动态语言之上,其自身存在的一些缺陷使得这项技术无法广泛使用,并进入主流。
    Runtime Unbound多态面临的第一个问题就是类型安全。确切的讲是静态类型安全。
    本质上,Runtime Unbound多态(动态语言)并非没有类型安全。当动态语言试图访问一个未知类型对象的成员时,会通过一些特殊机制或特殊接口获得类型信息,并在其中寻 找所需的对象成员。如果没有找到,便会抛出异常。但是,传统上,我们希望语言能够在编译期得到类型安全保证,而不要在运行时才发现问题。也就是说, Runtime Unbound多态只能提供运行时类型安全,而无法得到静态类型安全。
    第二个问题是性能。Runtime Unbound需要在运行时搜寻类型的接口,并执行调用。执行这类寻找和调用的方法有两种:反射和动态链接。
    反射机制可以向程序提供类型的信息。通过这些信息,Runtime Unbound可以了解是否存在所需的接口函数。反射通常也提供了接口函数调用的服务,允许将参数打包,并通过函数名调用。这种机制性能很差,基本上无法用于稍许密集些的操作。
    动态链接则是在访问对象前在对象的成员函数表上查询并获得相应函数的地址,填充到调用方的调用表中,调用方通过调用表执行间接调用。这种机制相对快一些,但由于需要查询成员函数表,复杂度基本上都在O(n)左右,无法与动多态的O(1)调用相比。
    这些问题的解决,依赖于一种新兴的技术,即concept。concept不仅很消除了类型安全的问题,更主要的是它大幅缩小了两种Runtime多态的性能差距,有望使Runtime Unbound成为主流的技术。

concept

    随着C++0x逐渐浮出水面,concept作为此次标准更新的核心部分,已经在C++社群中引起关注。随着时间的推移,concept的潜在作用也在不断被发掘出来。
    concept主要用来描述一个类型的接口和特征。通俗地讲,concept描述了一组具备了共同接口的类型。在引入concept后,C++可以对模板参数进行约束:
    concept assignable<T> {
        T& operator=(T const&);
    }
    template<assignable T> void copy(T& a, T const& b) {
        a=b;
    }
    这表示类型T必须有operator=的重载。如果一个类型X没有对operator=进行重载,那么当调用copy时,便会引发编译错误。这使得类型参数可以在函数使用之前便能得到检验,而无需等到对象被使用时。
    另一方面,concept参与到特化中后,使得操作分派更加方便:
    concept assignable<T> {
        T& operator=(T const&);
    }
    concept copyable<T> {
        T& T::copy(T const&);
    }
    template<assignable T> void copy(T& a, T const& b) {    //#1
        a=b;
    }
    template<copyable T> void copy(T& a, T const& b) {    //#2
        a.copy(b);
    }
    X x1,x2; //X支持operator=操作符
    Y y1,y2; //Y拥有copy成员函数
    copy(x1, x2);    //使用#1
    copy(y1, y2);    //使用#2
    在静多态中,concept很好地提供了类型约束。既然同样是Unbound,那么concept是否同样可以被用于Runtime Unbound?应当说可以,但不是现有的concept。在Runtime Unbound多态中,需要运行时的concept。
    依旧使用绘图案例做一个演示。假设这里使用的"C++"已经支持concept,并且也支持了运行时的concept:

    class Rectangle

    {

        void load(xml init) {...}

        void draw(monitor dev) {...}

        ...

    };

    class Ellipse

    {

        void load(xml init) {...}

        void draw(monitor dev) {...}

        ...

    };

    ...

    concept Shape<T> {

        void T::load(xml init);

        void T::draw(monitor dev);

    }

    ...

    void DrawShapes(monitor dev, vector<Shape> const& g)

    {

        vector<IShape>::const_iterator b(g.begin()), e(g.end());

        for(; b!=e; ++b)

        {

            (*b).draw(dev);

        }

    }

    ...

    vector<Shape> vg;

    vg.push_back(Rectangle(...));

    vg.push_back(Ellipse(...));

    vg.push_back(string("xxx"));    //错误,不符合Shape concept

    ...

    DrawShapes(crt, vg);

    乍看起来没什么特别的,但是请注意vector<Shape>。这里使用一个concept,而不是一个具体的类型,实例化一个模板。这里的意思是说,这个容器接受的是所有符合Shape concept的对象,类型不同也没关系。当push进vg的对象不符合Shape,便会发生编译错误。

     但是,最关键的东西不在这里。注意到DrawShapes函数了吗?由于vector<Shape>中的元素类型可能完全不同。语句 (*b).draw(dev);的语义在静态语言中是非法的,因为我们根本无法在编译时具体确定(*b)的类型,从而链接正确的draw成员。而在这里, 由于我们引入了Runtime Unbound,对于对象的访问链接发生在运行时。因此,我们便可以把不同类型的对象存放在一个容器中。

    concept在这里起到了类型检验的作用,不符合相应concept的对象是无法放入这个容器的,从而在此后对对象的使用的时候,也不会发生类型失配的 问题。这也就在动态的机制下确保了类型安全。动多态确保类型安全依靠静态类型。也就是所有类型都从一个抽象接口上继承,从而将类型归一化,以获得建立在静 态类型系统之上的类型安全。而concept的类型安全保证来源于对类型特征的描述,是一种非侵入的接口约束,灵活性大大高于类型归一化的动多态。

    如果我们引入这样一个规则:如果用类型创建实例(对象),那么所创建的对象是静态链接的,也就是编译时链接;而用concept创建一个对象,那么所创建的对象是动态链接的,也就是运行时链接。

    在这条规则的作用下,下面这段简单的代码将会产生非常奇妙的效果:

    class nShape

    {

    public:

        nShape(Shape g, int n) : m_graph(g), m_n(n) {}

        void setShape(Shape g) {

            m_graph=g;

        }

    private:

        Shape    m_graph;

     &