GP技术的展望——C--

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/longshanks/article/details/2759240
GP技术的展望——C--
莫华枫

    C++的复杂是公认的,尽管我认为在人类的聪明智慧之下,这点复杂压根儿算不上什么。不过我得承认,对于一般的应用而言,C++对程序员产生的压力还是不 小的。毕竟现在有更多更合适的选择。仅仅承认复杂,这没有什么意义。我不时地产生一个念头:有什么办法既保留C++的优点,而消除它的缺点和复杂。我知道 D语言在做这样的事情。但是,D更多地是在就事论事地消除C++的缺陷,而没有在根本上消除缺陷和复杂性。
    一般而言,一样东西复杂了,基本上都是因为东西太多。很显然,C++的语言特性在众多语言中是数一数二的。于是,我便想到或许把C++变成“C--”,可以治好C++的复杂之病。在探讨这个问题之前,让我们先从C出发,看看C++为何如此复杂。

C和C++

    尽管存在这样那样的不足,比如non-lalr的语法、隐式的指针类型转换等,但C语言的设计哲学却是足够经典的。C语言有一个非正式的分类,认为它既非汇编这样的低级语言,也非Pascal那样的高级语言, 而应该算作中级语言,介于其他两类语言之间。这种分类恰如其分地点出了C语言的特点和理念:以高级语言语法形式承载了低级语言的编程模型。低级语言的特点 是可以直接地描述硬件系统的结构。C则继承了这个特点。C语言直观地反映了硬件的逻辑构造,比如数组是内存块,可以等价于指针。在C语言中,我们可以几乎 直接看到硬件的构造,并且加以操作。这些特性对于底层开发至关重要。
    然而,C的这种直观简洁的模型过于底层和琐碎,不利于应用在那些构造复杂、变化多样的应用中。针对C的这些弱点,Bjarne Stroustrup决心利用OOP技术对C语言进行改造,从而促使了C++的诞生。C++全面(几乎100%)地兼容C,试图以此在不损失C语言的直观 和简洁的情况下,同时具备更强的软件工程特性,使其具备开发大型复杂系统的优势。这个目标“几乎”达到了,但是代价颇为可观。
    在经历了80、90年代的辉煌之后,C++的应用领域开始退步。一方面,在底层应用方面,C++的很多特性被认为是多余的。如果不使用这些特性,那么 C++则同C没有什么差别。相反这些特性的使用,对开发团队的整体能力提出了更高的要求。因而,在最底层,很多人放弃了C++而回归了C,因为那些高级特 性并未带来很多帮助,反而产生了很多负担。另一方面,在高层开发中,业务逻辑和界面也无需那么多底层的特性和苛刻的性能要求,更多简单方便、上手容易的语 言相比C++更加适合。C++的应用被压缩在中间层,随着业界系统级开发的不断专业化,C++的使用规模也会越来越小。(当然,它所开发的应用往往都是关 键性的,并且是没有选择余地的)。实际上,C++在这个层面也并非完美的工具。目前无法取代是因为没有相同级别的替代品。D或许是个强有力的竞争者,但一 方面出于历史遗留代码的规模和应用惯性,另一方面D也并未完全解决C++面临的复杂性问题,D也很难在可见的将来取代C++。
    实际上,C++的这种尴尬地位有着更深层次的原因。C++的本意是在保留C的底层特性基础上,增加更好的软件工程特性。但是,C++事实上并未真正意义上 地保留C的底层特性。回顾一下C的设计理念——直观而简洁地反映底层硬件的特性。C++通过兼容C获得了这种能力。但是这里有个问题,如果我要获得C的这 种简单直观性,就必须放弃C++中的很多高级特性。这里最明显的一个例子便是pod(plain old data)。
    在C中压根没有pod的概念,因为所有的对象都是pod。但是,C++中有了pod。因为C++的对象可能不是一个pod,那么我们便无法象在C当中那样 获得直观简洁的内存模型。对于pod,我们可以通过对象的基地址和数据成员的偏移量获得数据成员的地址,或者反过来。但在非pod对象中,却无法这么做。 因为C++的标准并未对非pod对象的内存布局作出定义,因而对于不同的编译器,对象布局是不同的。而在C中,仅仅会因为底层硬件系统的差异而影响对象布 局。
    这个问题通常并不显而易见。但在很多情况下为我们制造了不小的障碍。比如,对象的序列化:我们试图将一个对象以二进制流的形式保存在磁盘中、数据库中,或 者在网上传输,如果是pod,则直接将对象从基地址开始,按对象的大小复制出来,或传输,或存储,非常方便。但如果是非pod,由于对象的不同部分可能存 在于不同的地方,因而无法直接复制,只能通过手工加入序列化操作代码,侵入式地读取对象数据。(这个问题不仅仅存在于C++,其他语言,如java、C# 等都存在。只是它们没有很强烈的性能要求,可以使用诸如reflect等手段加以处理)。同样的问题也存在于诸如hash值的计算等方面。这对很多开发工 作造成不小的影响,不仅仅在底层,也包括很多高层的应用。
    究其原因,C++仅仅试图通过机械地将C的底层特性和OOP等高层特性混合在一起,意图达到两方兼顾的目的。但是,事与愿违,OOP的 引入实际上使得C的编程模型和其他更高级的抽象模型无法兼容。在使用C++的过程中,要么只使用C的特性,而无法获得代码抽象和安全性方面的好处,要么放 弃C的直观简洁,而获得高层次的抽象能力。反而,由于C和OOP编程模型之间的矛盾,大大增加了语言的复杂性和缺陷数。

舍弃

    但是,我们可以看到在C++中,并非所有的高级特性都与C的底层特性相冲突。很多使用C而不喜欢C++的人都表示过他们原意接受OB,也就是仅仅使用封装 。对于RAII,基本上也持肯定的态度。或许也会接受继承,但也表露出对这种技术带来的复杂性的担心。动多态是明显受到排斥的技术。显然这是因为动多态破坏了C的编程模型,使得很多本来简单的问题复杂化。不是它不好,或者没用,是它打破了太多的东西。
    因而,我们设想一下,如果我们去除动多态特性,那么是否会消除这类问题呢?我们一步步看。
    动多态的一个基本支撑技术是虚函数。在使用虚函数的情况下,类的每一次继承都会产生一个虚函数表(vtable),其中存放的是指向虚函数的指针。这些虚函数表必须存放在对象体中,也就是和对象的数据存放在一起(至少要关联在一起)。因而,对象在内存里并不是以连续的方式存放,而被分割成不同的部分,甚至身首异处(详见《Inside C++ Object Model》)。这便造成了前面所说的非pod麻烦。一旦放弃虚函数和vtable,对象的内存布局中,便不会有东西将对象分割开。所有的对象的数据存储都是连续的,因而都是pod。在这一点上,通过去除vtable,使得语言回归了C的直观和简单。
    动多态的内容当然不仅仅是一个虚函数,另一个重要的基石是继承。当然,我们并不打算放弃继承,因为它并不直接破坏C的直观性和简洁性。不同于虚函数,继承 不是完全为了动多态而生的。继承最初的用途在于代码复用。当它被赋予了多态含义后,才会成为动多态的基础。以下的代码可以有两种不同的解读:
    class B : public A {};
    从代码复用的角度来看,B继承自A,表示我打算让B复用A的所有代码,并且增加其他功能。而从多态的角度来看,B是一个A的扩展,B和A之间存在is-a的 关系。(B是一个A)。两者是站在不同层面看待同一个问题。代码复用,代表了编码的观点,而多态,则代表了业务逻辑的观点。但是,两者并非实质上的一回 事。在很多情况下,基类往往作为继承类的某种代表,或者接口,这在编码角度来看并没有对等的理解。而这种接口作用,则是动多态的基础。动多态通过不同的类 继承自同一个基类,使它们拥有共同的接口,从而可以使用统一的形式加以操作。作为一个极端,interface(或者说抽象基类),仅仅拥有接口函数(即vtable)而不包含任何数据成员。这是纯粹的接口。
    然而,这里存在一个缺陷。一个接口所代表的是一组类,它将成为这一组类同外界交互的共同界面。但是,使用基类、或者抽象基类作为接口,实质上是在使用一个 类型来代表一组类型。打个比方,一群人凑在一起出去旅游,我们称他们这群人为“旅行团”。我们知道旅行团不是一个人,而是一个不同于“人”的概念。动多态 里的接口相当于把一个旅行团当作一个人来看待。尽管这只是逻辑上的,或许一个旅行团的很多行为和一个人颇为相似。但是根本上而言,两者毕竟不是相同层次的 概念。这样的处理方法往往会带来了很多弊端。
    为了使继承被赋予的这重作用发挥作用,还需要一项非常关键的处理:类型转换。请看以下代码:
    void func(A* a);
    B b;
    func(&b);
    最后这行代码施行了动多态,如果B override了A的虚函数的话。很显然,如果严格地从强类型角度而言,&b是不应当作为func的实参,因为两者类型不匹配。但是如果拒绝接 受&b作为实参,那么动多态将无法进行下去。因此,我们放宽了类型转换的限制:允许继承类对象的引用或指针隐式地转换成基类的引用或指针。这样, 形如func(&b);便可以顺理成章地成为合法的代码。
    然而,这也是有代价的:
    B ba[5];
    func(ba);
    后面这行函数调用实际上是一个极其危险的错误。假设在func()中,将形参a作为一个类型A的数组对待,那么当我们使用ba作为实参调用func()的 时候,会将ba作为A的 数组处理。我们知道,数组内部元素是紧挨着的,第二个元素的位置是第一个元素的基址加上元素的尺寸,以此类推。如果传递进来的对象数组是B类型的,而被作 为A类型处理,那么两者的元素位置将可能不同步。尽管B继承自A,但是B的尺寸很有可能大于A,那么从第二个元素起,a[1]的地址并非ba[1]的地 址。于是,当我们以a[1]访问ba时,实际上很可能在ba[0]的内部某个位置读取,而func()的代码还以为是在操作ba[1]。这便是C++中的 一个重要的陷阱——对象切割。这种错误相当隐蔽,危险性极大。
    由于C++试图保留C的编程模型,因而保留了指针-数组的等价性。这种等价性体现了数组的本质。这在C中是一项利器,并无任何问题。但在C++中,由于存 在了继承,以及继承类的隐式类型转换,使得这种原本滋补的特性成为了一剂毒药。换句话说,C++所引入的动多态破坏了C的直观性。

舍弃之后

    从上面的分析来看,动多态同C的编程模型是不相容的。因而如果希望得到C的直观性,并且消除C++的缺陷,必须放弃动多态这个特性。现在来看看放弃之后将会怎样。
    一旦放弃了动多态,也就放弃了虚函数和vtable。此时,所有的对象都是pod了。那么首当其冲的好处,就是可以进行非侵入的序列化、hash计算等等 操作。由于对象肯定是连续分布的,可以直接地将对象取出进行编码、存储、计算和传输,而无需了解对象内部的数据结构和含义。另外一个重要的问题也会得到解 决,这就是ABI。在C中统一的ABI很自然地存在于语言中。我们可以很容易地用link将两个不同编译器编译的模块连接起来,而不会发生问题。但 是,C++中做不到,除非不再使用类而使用纯C。目前C++还没有统一的ABI,即便标准委员会有意建立这样的规范,实现起来也绝非易事。但是,如果放弃 动多态之后,对象的布局便回归到C的形态,从而使得ABI不再成为一个问题。
    另一方面,随着动多态的取消,那么继承的作用被仅仅局限于代码复用,不再具有构造接口的作用。我们前面已经看到,继承类向基类的隐式转换,是为了使基类能 够顺利地成为继承类的接口。既然放弃了动多态,那么也就无需基类再承担接口的任务。那么由继承类向基类的隐式类型转换也可以被禁止:
    void func(A* a);
    B b;
    func(&b);  //编译错误,类型不匹配
    进而对象切割也不会发生:
    B ba[5];
    func(ba); //编译错误,类型不匹配
    尽管根据数组-指针的等价性,ba可以被隐式地转换为B*,但是B*不再能够隐式地转换为A*,从而避免了对象的切割。
    问题是,如此简单地将动多态放弃掉,就如同将水和孩子一起泼掉那样,实际上放弃了动多态带来的好处。实际上并非如此。我们放弃动多态这个特性,但并不打算放弃它所具有的功能,而是用另一种技术加以替代。这便是runtime concept(这里这里)。
    不同于以类型为基础的interface,concept是独立于类型的系统。concept生来便是为了描述一组类型,因而是接口最理想的实现手段。当concept runtime化之后,便具有了与动多态相同的功能(很多方面还有所超越)。
    runtime concept同样需要类似vtable的函数分派表,但由于它不是类型,这些分派表无需存放在对象内部,可以独立放置(可以同RTTI信息放在一起), 并且只需一份。正是基于这个特性,方才保证了所有对象依然是pod,依然能够保证对象布局的直观性。
    同样,runtime concept承担了接口的任务,但不象动多态那样依赖于继承和相应的隐式类型转换。(通过自动或手动的concept_map)。因而,我们依旧可以禁止基于继承关系的隐式类型转换,从而防止对象切割的情况。
    一旦使用concept作为多态的实现手段,反倒促使原本动多态的一些麻烦得到消除。在动多态中,必须指定virtual函数。如此,在一个类中会存在两 种不同形态的函数,实现动多态的虚函数,和无此功能的普通函数。准确地维护这样两种函数,颇有些难度。而且,函数是虚还是不虚,牵涉到系统的设计,必须在 最初构建时确定,否则以后很难修改。但在放弃动多态,使用concept的情况下,只要一个继承类中,使用相同的签名覆盖基类中的函数,便实现了多态。当 进行concept_map,即将接口与类绑定时,只会考虑继承类的函数,而忽略基类中被覆盖的函数。于是,只需简单的覆盖,便实现了多态的控制。对于是 否多态一个函数,即是否改变基类函数的行为,完全由继承类控制,在创建基类时不必为此伤神。其结果就是,我们无需在系统设计的最初一刻就操心多态的问题, 而只需根据实现的需要随时实现。

其他

    存在大量隐式转换也是C++常受人诟病的一个方面,(特别是那些Pascal系的程序员)。隐式转换的目的是带来方便,使得编码更加简洁,减少冗余。同时也使得一些技巧得以施行。但是,隐式转换的副作用也颇为可观。比如:
    void fun(short a);
    long a=1248;
    fun(a); //顶多一个警告
    这种转换存在两面性:一方面,它可能是合理的,因为尽管a类型long大于short,但很可能存放着short可容纳的数值;但另一方面,a的确存在short无法容纳的可能性,这便会造成一个非常隐蔽的bug。
    C/C++对此的策略是把问题扔给程序员处理,如果有bug那是程序员的问题。这也算得上合情合理,毕竟有所得必有所失,也符合C/C++的一贯理念。但 终究不是最理想的方式。但是如果象Pascal那样将类型管得很死,那么语言又会失去灵活性,使得开发的复杂性增加。
    如果试图禁止隐式类型转换,那么为了维持函数使用代码的简洁性,函数必须对所有的类型执行重载。这大大增加了函数实现的负担,并且重复的代码严重违背了DRY原则。
    现在或许存在一些途径,使得在维持绝对强类型的情况下获得所希望的灵活性。钥匙可能依然在concept手上。考虑如下的代码:
    void fun(Integers a);
    long a=1248;
    fun(a);
    longlong b=7243218743012;
    fun(b);
    此处,fun()是一个函数,它的形参是一个concept,代表了所有的整型。这样,这个函数便可以接受任何一种整型(或者具有整型行为的类型)。我们 相信,在一般的应用下,任何整数都有完全相同的行为。因此,我们便可以很方便地使用Integers这个接口执行对整数的操作,而无需关心到底是什么样的 整数。
    如此,我们便可以在禁止隐式类型转换,不使用函数重载的情况下,完成这种函数的编写。同时可以得到更好的类型安全性。

    强制类型转换是非常重要的特性,特别是在底层开发时。但也是双刃剑,往往引来很隐蔽的错误。强制类型转换很多情况下是无理的,通常都是软件的设计问题造成的。但终究还是有一些情况,需要它来处理。
    设想这样一个场景:两个一模一样的类型,但它们分属不同的函数。(这种情形尽管不多见,但还是存在的。这往往是混乱设计的结果。当然也有合理的情况,比如 来自两个不同库的类型)。我现在需要写一个函数,能够同时使用这两个类型。比较安全一些的,可以用函数重载。但是两个重载的函数代码是一样的,典型的冗余 代码。当然也可以针对其中一个结构编写代码,然后在使用时,对另一个结构的实例执行强制类型转换。但是,强制类型转换毕竟不是件好事。因此,我们也可以构 造一个concept,让它描述这两个类型。然后在编写函数时使用这个concept,当这两个类型都与concept绑定后,便可以直接使用这两个类 型,而没有类型安全和代码冗余的问题。
    (顺便提一下,这种方式也可以运用在类型不同的情况下。比如两个类型不完全相同,但是基本要素都一样。那么就可以使用concept_map的适配功能,将两个类型统一在一个concept下。这种方式相比oop的Adapter模式,更加简洁。adapter本身是一个container,它所实现的接口函数,都必须一一转发到内部的对象,编写起来相当繁琐。但在concept_map中,对于那些符合concept描述的函数无需另行处理,concept会自动匹配,只需对那些不符合要求的函数执行适配。)

    前面说过,指针数组的等价性体现了一种直观的编程模型。但是,指针和数组毕竟还是存在很多差别,比如指针仅仅表达了一组对象在内存中的位置,但并未携带对象数量的信息。因而,当数组退化成指针时,便已经失去了数组的身份:
    void func(int* x);
    int a[20];
    func(a);
    这里,在函数func中已经无法将a作为数组处理,因为无法知道变成int*后的a有多大来避免越界。甚至我们无法把a作为多个对象构成的内存块看待,因为我们不知道大小。因此,只有显式地给出数组大小,才能使用:
    void func(int* x, long size);
    但是,在concept的作用下,数组和指针得以依然保持它们的等价性的情况下,解决数组退化问题。考虑这样两个函数:
    void func1(Pointer x);
    void func2(Container x);
    其中,Pointer是代表指针的concept,而Container则是代表容器的concept。必须注意的是,Pointer是严格意义上的指 针,也就是说无法在Pointer上执行迭代操作。Pointer只能作为指针使用,只具备dereference的能力(很像java的“指针”,不是 吗?concept在没有放弃C的底层特性的情况下也做到了。)。而Container则是专门用来表达容器的concept,其基本的特性便是迭代。在 func1中,无法对形参x执行迭代,仅仅将其作为指向一个对象的指针处理,保证其安全性。而对于需要进行迭代操作的func2而言,x则是可以遍历的。 于是,对于同一个数组a,两个函数分别从不同的角度对其进行处理:
    int a[20];
    func1(a); //a直接作为指针处理,但不能迭代
    func2(a); //a作为容器处理,可以迭代,并且其尺寸信息也一同传入
    此处实际上是利用了concept对类型特性的描述作用,将具有两重性的数组类型(数组a即代表了数组这个容器,也代表了数组的起始地址)以不同特征加以 表达,以满足不同应用的需求。数组仍然可以退化成指针,C的直观模型得到保留,在很多特殊的场合发挥作用。但在其他应用场景,可以更加安全地使用数组。
   

总结

    综上所述,C++未能真正延续C的直观简洁,主要是由于动多态的一些基础设施破坏了C的编程模型。因而,我们可以通过放弃动多态,及其相关的一些技术,代 之以更加“和谐”的runtime concept,使得C++在基本保留C的编程模型的同时,获得了相比原来更好的软件工程特性。至此,这种改变后的C++(如果还能称为C++的话)拥有 如下的主干特性:
    1、SP,来自于C。
    2、完全pod化。
    3、OB。保留了封装和RAII。尽管也保留了继承,但其作用仅限于代码复用,禁止基于继承的隐式类型转换。
    4、GP,包括static和runtime concept。这是抽象高级特性的核心和基石。
    这样的语言特性实质上比现有的C++更加简洁,但是其能力更加强大。也比C++更易于贴近C的编程模型,以便适应底层的开发。我不能说这样的变化是否会产生一个更好的语言,但是我相信这些特性有助于构造更加均衡统一的语言。
展开阅读全文

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

01-06

GP技术的展望——道生一,一生二rnrnby 莫华枫rnrn 长期以来,我们始终把GP(泛型编程)作为一种辅助技术,用于简化代码结构、提高开发效率。从某种程度上来讲,这种观念是对的。因为迄今为止,GP技术还只是一种编译期技术。只能在编译期发挥作用,一旦软件完成编译,成为可执行代码,便失去了利用GP的机会。对于现在的多数应用而言,运行时的多态能力显得尤为重要。而现有的GP无法在这个层面发挥作用,以至于我这个“GP迷”也不得不灰溜溜地声称“用OOP构建系统,用GP优化代码”。rn 然而,不久前,在TopLanguage group上的一次讨论,促使我们注意到runtime GP这个概念。从中,我们看到了希望——使GP runtime化的希望——使得GP有望在运行时发挥其巨大的威力,进一步为软件的设计与开发带来更高的效率和更灵活的结构。rn 在这个新的系列文章中,我试图运用runtime GP实现一些简单,但典型的案例,来检测runtime GP的能力和限制,同时也可以进一步探讨和展示这种技术的特性。rn运行时多态rn 现在的应用侧重于交互式的运作形式,要求软件在用户输入下作出响应。为了在这种情况下,软件的整体结构的优化,大量使用组件技术,使得软件成为“可组装” 的系统。而接口-实现分离的结构形式很好地实现了这个目标。多态在此中起到了关键性的作用。其中,以OOP为代表的“动多态”(也称为 “subtyping多态”),构建起在运行时可调控的可组装系统。GP作为“静多态”,运用泛化的类型体系,大大简化这种系统的构建,消除重复劳动。另外还有一种鲜为人知的多态形式,被《C++ Template》的作者David Vandevoorde和Nicolai M. Josuttis称为runtime unbound多态。而原来的“动多态”,即OOP多态,被细化为runtime bound多态;“静多态”,也就是模板,则被称为static unbound多态。rn 不过这种称谓容易引起误解,主要就是unbound这个词上。在这里unbound和bound是指在编写代码时,一个symbol是否同一个具体的类型 bound。从这点来看,由于GP代码在编写之时,面向的是泛型,不是具体的类型,那么GP是unbound的。因为现有的GP是编译期的技术,所以是 static的。OOP的动多态则必须针对一个具体的类型编写代码,所以是bound的。但因为动多态可以在运行时确定真正的类型,所以是runtime 的。至于runtime unbound,以往只出现在动态语言中,比如SmallTalk、Python、Ruby,一种形象地称谓是“duck-typing”多态。关于多态的更完整的分类和介绍可以看这里。rn 通过动态语言机制实现的runtime unbound,存在的性能和类型安全问题。但当我们将GP中的concept技术推广到runtime时会发现,rungime unbound可以拥有同OOP动多态相当的效率和类型安全性,但却具有更大的灵活性和更丰富的特性。关于这方面,我已经写过一篇文章 ,大致描述了一种实现runtime concept的途径(本文的附录里,我也给出了这种runtime concept实现的改进)。rnRuntime Conceptrn Runtime concept的实现并不会很复杂,基本上可以沿用OOP中的“虚表”技术,并且可以更加简单。真正复杂的部分是如何在语言层面表达出这种runtime GP,而不对已有的static GP和其他语言特性造成干扰。在这里,我首先建立一个基本的方案,然后通过一些案例对其进行检验,在发现问题后再做调整。rn 考虑到runtime concept本身也是concept,那么沿用static concept的定义形式是不会有问题的:rn concept myconcept rn T& copy(T& lhs, T const& rhs);rn void T::fun();rn ...rn rn 具体的concept定义和使用规则,可以参考C++0x的concept提案或这篇文章 ,以及这篇文章 。rn 另一方面,我们可以通过concept_map将符合一个concept的类型绑定到该concept之上:rn concept_map myconcept rn 相关内容也可参考上述文件。rn 有了concept之后,我们便可以用它们约束一个模板:rn templatevoid myfun(T const& val); //函数模板rn templateclass X //类模板rn rn ...rn ;rn 到此为止,runtime concept同static concept还是同一个事物。它们真正的分离在于使用。对于static concept应用,我们使用一个具体的类型在实例化(特化)一个模板:rn X x1; //实例化一个类模板rn MyType obj1;rn myfun(obj1); //编译器推导obj1对象的类型实例化函数模板rn myfun(obj1); //函数模板的显式实例化rn 现在,我们将允许一种非常规的做法,以使runtime concept成为可能:允许使用concept实例化一个模板,或定义一个对象。rn X x2;rn myconcept* obj2=new myconcept;rn myfun(obj2); //此处,编译器将会生成runtime版本的myfunrn 这里的含义非常明确:对于x2,接受任何符合myconcept的类型的对象。obj2是一个“动态对象”(这里将runtime concept引入的那种不知道真实类型,但符合某个concept的对象称为“动态对象”。而类型明确已知的对象成为“静态对象”),符合myconcept要求。至于实际的类型,随便,只要符合myconcept就行。rn 这种情形非常类似于传统动多态的interface。但是,它们有着根本的差异。interface是一个具体的类型,并且要求类型通过继承这种形式实现这个接口。而concept则不是一种类型,而是一种“泛型”——具备某种特征的类型的抽象(或集合),不需要在类型创建时立刻与接口绑定。与 concept的绑定(concept_map)可以发生在任何时候。于是,runtime concept实际上成为了一种非侵入的接口。相比interface这种侵入型的接口,更加灵活便捷。rn 通过这样一种做法,我们便可以获得一种能够在运行时工作的GP系统。rn 在此基础上,为了便于后续案例展开,进一步引入一些有用的特性:rnrn 1. 一个concept的assosiate type被视为一个concept。一个concept的指针/引用(concept_id*/concept_id&,含义是指向一个符合concept_id的动态对象,其实际类型未知),都被视作concept。一个类模板用concept实例化后,逻辑上也是一个concept。rn 2. 动态对象的创建。如果需要在栈上创建动态对象,那么可以使用语法:concept_id obj_id; 这里concept_id是concept名,type_id是具体的类型名,obj_id是对象名称。这样,便在栈上创建了一个符合concept_id的动态对象,其实际类型是type_id。rn 如果需要在堆上创建动态对象,那么可以用语法:concept_id* obj_id=new concept_id; 这实际上可以看作“concept指针/引用”。rn 3. concept推导(编译期)。对于表达式concept_id obj_id=Exp,其中Exp是一个表达式,如果表达式Exp的类型是具体的类型,那么obj_id代表了一个静态对象,其类型为Exp的类型。如果表达式Exp的类型是concept,那么obj_id是一个动态对象,其类型为Exp所代表的concept。rn 那么如何确定Exp是具体类型还是concept?可以使用这么一个规则:如果Exp中涉及的对象,比如函数的实参、表达式的操作数等等,只要有一个是动态对象(类型是concept),那么Exp的类型就是concept;反之,如果所有涉及的对象都是静态对象(类型为具体的类型),那么Exp的类型为相应的具体类型。同样的规则适用于concept*或concept&。rn 4. concept转换。类似在类的继承结构上执行转换。refined concept可以隐式地转换成base concept,反过来必须显式地进行,并且通过concept_cast操作符执行。兄弟concept之间也必须通过concept_cast转换。rn 5. 基于concept的重载,也可以在runtime时执行,实现泛化的dynamic-dispatch操作。rnrnrn 下面,就开始第一个案例。rn案例:升级的坦克rn 假设我们做一个游戏,主题是开坦克打仗。按游戏的惯例,消灭敌人可以得到积分,积分到一定数量,便可以升级。为了简便起见,我们只考虑对主炮升级。第一级的主炮是90mm的;第二级的主炮升级到120mm。主炮分两种,一种只能发射穿甲弹,另一种只能发射高爆弹。因此,坦克也分为两种:能打穿甲弹的和能打高爆弹的。rn 为了使代码容易开发和维护,我们考虑采用模块化的方式:开发一个坦克的框架,然后通过更换不同的主炮,实现不同种类的坦克和升级:rn //一些基本的concept定义rn //炮弹头conceptrn concept Warheads rn double explode(TargetType tt); //炮弹爆炸,返回杀伤率。不同弹头,对不同类型目标杀伤率不一样。rn rn //炮弹concept,我们关心的当然是弹头,所以用Warheads定义一个associate typern concept Rounds rn Warheads WH;rn ...rn rn //主炮conceptrn concept Cannons rn Rounds R;rn void T::load(R& r); //装填炮弹,load之后炮弹会存放在炮膛里,不能再load,除非把炮弹打出去rn R::WH T::fire(); //开炮,返回弹头。发射后炮膛变空,可以再loadrn rn //类型和模板定义rn //坦克类模板rn templatern class Tankrn rn ...rn public:rn void load(typenam C::R& r) rn m_cannon.load(r);rn rn typename C::R::WH fire() rn return m_cannon.fire();rn rn private:rn C m_cannon;rn ;rn //主炮类模板rn templatern class Cannonrn rn public:rn typedef Rd R;rn void load(R& r) ...rn typename R::WH fire() ...rn rn template concept_map rn //炮弹类模板rn templatern class Roundrn rn public:rn typedef W WH;rn static const int caliber=W::caliber;rn W shoot() ...rn ...rn ;rn template concept_map>rn 论坛

GP技术展望——先有鸿钧后有天

07-26

[align=center][size=18px]GP技术的展望——先有鸿钧后有天[/size][/align]rn[align=center]莫华枫[/align]rnrn 自从高级语言出现以来,类型始终是语言的核心。几乎所有语言特性都要以类型作为先决条件。类型犹如天地,先于万物而存在。但是,是否还有什么东西比类型更加原始,更加本质,而先于它存在呢?请往下看。:)rn[b]泛型和类型[/b]rn 泛型最简短最直观的描述恐怕应该是:the class of type。尽管这样的描述不算最完备,但也足以说明问题。早在60年代,泛型的概念便已经出现。最初以“参数化类型”的名义存在。70年代末期发展起来的恐龙级的Ada(我的意思不是说Augusta Ada Byron Lovelace伯爵夫人是恐龙,从画像上看,这位程序员的祖师奶长得相当漂亮:)),尚未拥有oop(Ada83),便已经实现了泛型(Generic)。尽管泛型历史悠久,但真正全面地发展起来,还是在90年代初,天才的Alexander A. Stepanov创建了stl,促使了“泛型编程”(Generic Programming)的确立。rn 出于简便的目的,我套用一个老掉牙的“通用容器”来解释泛型的概念。(就算我敷衍吧:P,毕竟重头戏在后面,具体的请看前面给出的链接)。假设我在编程时需要一个int类型的栈,于是我做了一个类实现这个栈:rn class IntStack ...;rn 用的很好。过了两天,我又需要一个栈,但是类型变成了double。于是,我再另写一个:rn class DoubleStack ...;rn 不得了,好象是通了马蜂窝,不断地出现了各种类型的栈的需求,有string的,有datetime的,有point的,甚至还有一个Dialog的。每种类型都得写一个类,而且每次代码几乎一样,只是所涉及的类型不同而已。于是,我就热切地期望出现一种东西,它只是一个代码的框架,实现了stack的所有功能,只是把类型空着。等哪天我需要了,把新的类型填进去,便得到一个新的stack类。rn 这便是泛型。rn 但是,仅仅这些,还不足以成就GP的威名。rn 我有一个古怪的需求(呵呵,继续敷衍。:)):rn 做一个模板,内部有一个vector<>成员:rn template Arn rn ...rn vector m_x;rn ;rn 可是,如果类型实参是int类型的话,就得用set<>。为了使用的方便,模板名还得是A。于是,我们就得使用下面的技巧:rn template<> Arn rn ...rn set m_x;rn ;rn 这叫特化(specialization),相当于告诉编译器如果类型实参是int,用后面那个。否则,用前面的。特化实际上就是根据类型实参由编译器执行模板的选择。换句话说,特化是一种编译期分派技术。rn 这里还有另一个更古怪需求:如果类型实参是指针的话,就用list<>。这就得用到另一种特化了:rn template Arn rn ...rn list m_x;rn rn 这是局部特化(partial specialization),而前面的那种叫做显式特化(explicit specialization),也叫全特化。局部特化则是根据类型实参的特征(或者分类)执行的模板选择。rn 最后,还有一个最古怪的需求:如果类型实参拥有形如void func(int a)成员函数的类型,那么就使用deque。这个...,有点难。现有的C++编译器,是无法满足这个要求的。不过希望还是有的,在未来的新版C++09中,我们便可以解决这个问题。rn[b]concept和类型[/b]rn concept是GP发展必然结果。正如前面所提到的需求,我们有时候会需要编译器能够鉴识出类型的某些特征,比如拥有特定的成员等等,然后执行某种操作。下面是一个最常用的例子:rn swap()是一个非常有用的函数模板,它可以交换两个对象的内容,这是swap手法的基础。swap()的基本定义差不多是这样:rn template swap(T& lhs, T& rhs) rn T tmp(lhs);rn lhs=rhs;rn rhs=tmp;rn rn 但是,如果需要交换的对象是容器之类的大型对象,那么这个swap()的性能会很差。因为它执行了三次复制,这往往是O(n)的。标准容器都提供了一个 swap成员函数,通过交换容器内指向数据缓冲的指针,获得O(1)的性能。因此,swap()成员是首选使用的。但是,这就需要程序员识别对象是否存在 swap成员,然后加以调用。如果swap()函数能够自动识别对象是否存在swap成员,那么就可以方便很多。如果有swap成员,就调用成员,否则,就是用上述通过中间变量交换的版本。rn 这就需要用到concept技术了:rn template void swap(T& lhs, T& rhs) rn lhs.swap(rhs);rn rn 这里,Swappable是一个concept:rn concept Swappable rn void T::swap(T&);rn rn 于是,如果遇到拥有swap成员函数的对象,正好符合Swappable concept,编译器可以使用第二个版本,在O(1)复杂度内完成交换。否则,便使用前一个版本:rn vector a, b;rn ... //初始化a和brn swap(a,b); //使用后一个版本rn int c=10, d=23;rn swap(c, d); //使用前一个版本rn 这里的swap()也是运用了特化,所不同的是在concept的指导下进行的。这样的特化有时也被称作concept based overload。rn 从上面的例子中可以看到,原先的特化,无论是全特化,还是局部特化,要么特化一个类型,要么特化一个大类(如指针)的类型。但无法做到更加精细。比如,我希望一个模板能够针对所有的整数(int,long,short,char等)进行特化,这在原先是无法做到的。但拥有了concept之后,我们便可以定义一个代表所有整数的concept,然后使用这个整数concept执行特化。换句话说,concept使得特化更加精细了,整个泛型系统从原来“离散”的变成了“连续”的。rn 不过上面那个concept特化的模板看起来实在不算好看,头上那一坨template...实在有碍观瞻。既然是concept based overload,那么何不直接使用重载的形式,而不必再带上累赘的template<...>:rn void fun(anytype a)... //#1,anytype是伪造的关键字,表示所有类型。这东西最好少用。rn void fun(Integers a)... //#2,Integers是concept,表示所有整型类型rn void fun(Floats a)... //#3,Floats是concept,表示所有浮点类型rn void fun(long a)... //#4rn void fun(int a)... //#5rn void fun(double a)... //#6rn ...rn int x=1;rn long y=10;rn short z=7;rn string s="aaa";rn float t=23.4;rn fun(x); //选择#5rn fun(y); //选择#4rn fun(z); //选择#2rn fun(s); //选择#1rn fun(t); //选择#3rn 这种形式在语义上与原来的模板形式几乎一样。注意,是几乎。如下的情形是重载形式无法做到的:rn template T swap(T lhs, T rhs) rn T temp(lhs);rn ...rn rn 这里,模板做到了两件事:其一,模板萃取出类型T,在函数体中,可以使用T执行一些操作,比如上述代码中的临时对象temp的构造。这个问题容易解决,因为萃取类型T还有其他的方法,一个typeof()操作符便可实现:rn Integers swap(Integers lhs, Integers rhs) rn typeof(lhs) temp(lhs);rn ...rn rn 其二,模板保证了lhs,rhs和返回值都是同一类型。这个问题,可以通过施加在函数上的concept约束解决:rn Integers swap(Integers lhs, Integers rhs)rn requires SameTypern && SameType //retval是杜撰的关键字,用以表示返回值rn typeof(lhs) temp(lhs);rn ...rn rn 相比之下,重载形式比较繁琐。总体而言,尽管重载形式冗长一些,但含义更加明确,更加直观。并且在concept的接口功能作用下,对参数类型一致的要求通常并不多见(一般在基本类型,如整型等,的运算处理中较多见。因为这些操作要求类型有特定的长度,以免溢出。其他类型,特别是用户定义类型,通常由于封装的作用,不会对类型的内部特性有过多要求,否则就不应使用泛型算法)。如果可以改变语法的话,那么就能用诸如@代替typeof,==代替 SameType的方法减少代码量:rn Integers swap(Integers lhs, Integers rhs)rn requires @lhs == @rhs && @lhs == @retval rn @lhs temp(lhs);rn ...rn rn rn[b]concept、类型和对象[/b]rn 事情还可以有更加夸张的发展。前面对泛型进行了特化,能不能对类型也来一番“特化”呢?当然可以:rn void fun(int a);rn void fun(int a:a==0); //对于类型int而言,a==0便是“特化”了rn 更完整的,也可以有“局部特化”:rn void fun(int a); //#1rn void fun(int a:a==0); //#2rn void fun(int a:a>200); //#3rn void fun(int a:a<20&&a>10); //#4rn void fun(int a:(a>70&&a<90)||(a<-10)); //#5rn ...rn int a=0, b=15, c=250, d=-50;rn fun(80); //使用#5rn fun(50); //使用#1rn fun(a); //使用#2rn fun(b); //使用#4rn fun(c); //使用#3rn fun(d); //使用#5rn 实际上,这无非是在参数声明之后加上一组约束条件,用以表明该版本函数的选择条件。没有约束的函数版本在没有任何约束条件匹配的情况下被选择。对于使用立即数或者静态对象的调用而言,函数的选择在编译期执行,编译器根据条件直接调用匹配的版本。对于变量作为实参的调用而言,则需要展开,编译器将自动生成如下代码:rn //首先将函数重新命名,赋予唯一的名称rn void fun_1(int a); //#1rn void fun_2(int a); //#2rn void fun_3(int a); //#3rn void fun_4(int a); //#4rn void fun_5(int a); //#5rn //然后构造分派函数rn void fun_d(int a) rn if(a==0)rn fun_2(a);rn else if(a>200)rn fun_3(a);rn ...rn elsern fun_1(a);rn rn 在某些情况下,可能需要对一个对象的成员做出约束,此时便可以采用这种形式:rn struct Arn rn float x;rn ;rn ...rn void fun(A a:a.x>39.7);rn ...rn 这种施加在类型上的所谓“特化”实际上只是一种语法糖,只是由编译器自动生成了分派函数而已。这个机制在Haskell等语言中早已存在,并且在使用上带来很大的灵活性。如果没有这种机制,那么一旦需要增加函数分派条件,那么必须手工修改分派函数。如果这些函数,包括分派函数,是第三方提供的代码,那么修改将是很麻烦的事。而一旦拥有了这种机制,那么只需添加一个相应的函数重载即可。rn 当concept-类型重载和类型-对象重载混合在一起时,便体现更大的作用:rn void fun(anytype a);rn void fun(Integers a);rn void fun(Floats a);rn void fun(long a);rn void fun(int a);rn void fun(double a);rn void fun(double a:a==0.8);rn void fun(short a:a<10);rn void fun(string a:a=="abc");rn ...rn concept-类型-对象重载体系遵循一个原则:优先选择匹配的函数中最特化的。这实际上是类型重载规则的扩展。大的来说,所有类型比所属的 concept更加特化,所有对象约束比所属的类型更加特化。对于concept而言,如果concept A refine自concept B,那么A比B更加特化。同样,如果一个类型的约束强于另一个,那么前一个就比后一个更加特化,比如a==20比a>10更加特化。综合起来,可以有这样一个抽象的规则:两个约束(concept,或者施加在对象上的约束)A和B,作用在类型或者对象上分别产生集合,如果A产生的集合是B产生的集合的真子集,那么便认为A比B更加特化。rn 根据这些规则,实际上可以对一个函数的重载构造出一个“特化树”:rn[img=http://docs.google.com/File?id=dd4fc9w_43c5sh64dd_b][/img]rn 越接近树的根部,越泛化,越接近叶子,越特化。调用时使用的实参便在这棵“特化树”上搜索,找到最匹配的函数版本。rn concept-类型-对象体系将泛型、类型和对象统一在一个系统中,使得函数的重载(特化)具有更简单的形式和规则。并且,这个体系同样可以很好地在类模板上使用,简化模板的定义和使用。rn类模板rn C++的类模板特化形式并不惹人喜爱:rn template A...; //基础模板rn template<> A...; //显式特化(全特化)rn template A...; //局部特化rn 在C++09中,可以直接用concept定义模板的类型形参:rn template A...;rn 实质上,这种形式本身就是一种局部特化,因而原本那种累赘局部特化形式可以废除,代之以concept风格的形式:rn template A...; //Pointer表示此处采用指针特化模板rn 同样,如果推广到全特化,形式也就进一步简单了:rn template A...; //这个形式有些突兀,这里只打算表达这个意思,应该有更“和谐”的形式rn 如果模板参数是对象,则使用现有的定义形式:rn template A...;rn 更进一步,可以引入对象的约束:rn template10> A...;rn 此外,C++中在模板特化之前需要有基础模板。但实际上这是多余的,D语言已经取消了这个限制,这对于简化模板的使用有着莫大的帮助。rnrn 论坛

没有更多推荐了,返回首页