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

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

    自从高级语言出现以来,类型始终是语言的核心。几乎所有语言特性都要以类型作为先决条件。类型犹如天地,先于万物而存在。但是,是否还有什么东西比类型更加原始,更加本质,而先于它存在呢?请往下看。:)

泛型和类型

    泛型最简短最直观的描述恐怕应该是:the class of type。尽管这样的描述不算最完备,但也足以说明问题。早在60年代,泛型的概念便已经出现。最初以“参数化类型”的名义存在。70年代末期发展起来的 恐龙级的Ada(我的意思不是说Augusta Ada Byron Lovelace伯爵夫人是恐龙,从画像上看,这位程序员的祖师奶长得相当漂亮:)),尚未拥有oop(Ada83),便已经实现了泛型(Generic)。尽管泛型历史悠久,但真正全面地发展起来,还是在90年代初, 天才的Alexander A. Stepanov创建了stl,促使了“泛型编程”(Generic Programming)的确立。
    出于简便的目的,我套用一个老掉牙的“通用容器”来解释泛型的概念。(就算我敷衍吧:P,毕竟重头戏在后面,具体的请看前面给出的链接)。假设我在编程时需要一个int类型的栈,于是我做了一个类实现这个栈:
    class IntStack {...};
    用的很好。过了两天,我又需要一个栈,但是类型变成了double。于是,我再另写一个:
    class DoubleStack {...};
    不得了,好象是通了马蜂窝,不断地出现了各种类型的栈的需求,有string的,有datetime的,有point的,甚至还有一个Dialog的。每 种类型都得写一个类,而且每次代码几乎一样,只是所涉及的类型不同而已。于是,我就热切地期望出现一种东西,它只是一个代码的框架,实现了stack的所 有功能,只是把类型空着。等哪天我需要了,把新的类型填进去,便得到一个新的stack类。
    这便是泛型。
    但是,仅仅这些,还不足以成就GP的威名。
    我有一个古怪的需求(呵呵,继续敷衍。:)):
    做一个模板,内部有一个vector<>成员:
    template<typename T> A
    {
        ...
        vector<T> m_x;
    };
    可是,如果类型实参是int类型的话,就得用set<>。为了使用的方便,模板名还得是A。于是,我们就得使用下面的技巧:
    template<> A<int>
    {
        ...
        set<T> m_x;
    };
    这叫特化(specialization),相当于告诉编译器如果类型实参是int,用后面那个。否则,用前面的。特化实际上就是根据类型实参由编译器执行模板的选择。换句话说,特化是一种编译期分派技术。
    这里还有另一个更古怪需求:如果类型实参是指针的话,就用list<>。这就得用到另一种特化了:
    template<typename T> A<T*>
    {
        ...
        list<T> m_x;
    }
    这是局部特化(partial specialization),而前面的那种叫做显式特化(explicit specialization),也叫全特化。局部特化则是根据类型实参的特征(或者分类)执行的模板选择。
    最后,还有一个最古怪的需求:如果类型实参拥有形如void func(int a)成员函数的类型,那么就使用deque。这个...,有点难。现有的C++编译器,是无法满足这个要求的。不过希望还是有的,在未来的新版C++09中,我们便可以解决这个问题。

concept和类型

    concept是GP发展必然结果。正如前面所提到的需求,我们有时候会需要编译器能够鉴识出类型的某些特征,比如拥有特定的成员等等,然后执行某种操作。下面是一个最常用的例子:
    swap()是一个非常有用的函数模板,它可以交换两个对象的内容,这是swap手法的基础。swap()的基本定义差不多是这样:
    template<typename T> swap(T& lhs, T& rhs) {
        T tmp(lhs);
        lhs=rhs;
        rhs=tmp;
    }
    但是,如果需要交换的对象是容器之类的大型对象,那么这个swap()的性能会很差。因为它执行了三次复制,这往往是O(n)的。标准容器都提供了一个 swap成员函数,通过交换容器内指向数据缓冲的指针,获得O(1)的性能。因此,swap()成员是首选使用的。但是,这就需要程序员识别对象是否存在 swap成员,然后加以调用。如果swap()函数能够自动识别对象是否存在swap成员,那么就可以方便很多。如果有swap成员,就调用成员,否则, 就是用上述通过中间变量交换的版本。
    这就需要用到concept技术了:
    template<Swappable T> void swap(T& lhs, T& rhs) {
        lhs.swap(rhs);
    }
    这里,Swappable是一个concept:
    concept Swappable<typename T> {
        void T::swap(T&);
    }
    于是,如果遇到拥有swap成员函数的对象,正好符合Swappable concept,编译器可以使用第二个版本,在O(1)复杂度内完成交换。否则,便使用前一个版本:
    vector a, b;
    ... //初始化a和b
    swap(a,b); //使用后一个版本
    int c=10, d=23;
    swap(c, d); //使用前一个版本
    这里的swap()也是运用了特化,所不同的是在concept的指导下进行的。这样的特化有时也被称作concept based overload。
    从上面的例子中可以看到,原先的特化,无论是全特化,还是局部特化,要么特化一个类型,要么特化一个大类(如指针)的类型。但无法做到更加精细。比如,我 希望一个模板能够针对所有的整数(int,long,short,char等)进行特化,这在原先是无法做到的。但拥有了concept之后,我们便可以 定义一个代表所有整数的concept,然后使用这个整数concept执行特化。换句话说,concept使得特化更加精细了,整个泛型系统从原来“离 散”的变成了“连续”的。
    不过上面那个concept特化的模板看起来实在不算好看,头上那一坨template...实在有碍观瞻。既然是concept based overload,那么何不直接使用重载的形式,而不必再带上累赘的template<...>:
    void fun(anytype a){...} //#1,anytype是伪造的关键字,表示所有类型。这东西最好少用。
    void fun(Integers a){...} //#2,Integers是concept,表示所有整型类型
    void fun(Floats a){...} //#3,Floats是concept,表示所有浮点类型
    void fun(long a){...} //#4
    void fun(int a){...} //#5
    void fun(double a){...} //#6
    ...
    int x=1;
    long y=10;
    short z=7;
    string s="aaa";
    float t=23.4;
    fun(x); //选择#5
    fun(y); //选择#4
    fun(z); //选择#2
    fun(s); //选择#1
    fun(t); //选择#3
    这种形式在语义上与原来的模板形式几乎一样。注意,是几乎。如下的情形是重载形式无法做到的:
    template<Integers T> T swap(T lhs, T rhs) {
        T temp(lhs);
        ...
    }
    这里,模板做到了两件事:其一,模板萃取出类型T,在函数体中,可以使用T执行一些操作,比如上述代码中的临时对象temp的构造。这个问题容易解决,因为萃取类型T还有其他的方法,一个typeof()操作符便可实现:
    Integers swap(Integers lhs, Integers rhs) {
        typeof(lhs) temp(lhs);
        ...
    }
    其二,模板保证了lhs,rhs和返回值都是同一类型。这个问题,可以通过施加在函数上的concept约束解决:
    Integers swap(Integers lhs, Integers rhs)
        requires SameType<lhs, rhs>
            && SameType<lhs, retval> {  //retval是杜撰的关键字,用以表示返回值
        typeof(lhs) temp(lhs);
        ...
    }
    相比之下,重载形式比较繁琐。总体而言,尽管重载形式冗长一些,但含义更加明确,更加直观。并且在concept的接口功能作用下,对参数类型一致的要求 通常并不多见(一般在基本类型,如整型等,的运算处理中较多见。因为这些操作要求类型有特定的长度,以免溢出。其他类型,特别是用户定义类型,通常由于封 装的作用,不会对类型的内部特性有过多要求,否则就不应使用泛型算法)。如果可以改变语法的话,那么就能用诸如@代替typeof,==代替 SameType的方法减少代码量:
    Integers swap(Integers lhs, Integers rhs)
        requires @lhs == @rhs && @lhs == @retval {
        @lhs temp(lhs);
        ...
    }
   

concept、类型和对象

    事情还可以有更加夸张的发展。前面对泛型进行了特化,能不能对类型也来一番“特化”呢?当然可以:
    void fun(int a);
    void fun(int a:a==0); //对于类型int而言,a==0便是“特化”了
    更完整的,也可以有“局部特化”:
    void fun(int a); //#1
    void fun(int a:a==0); //#2
    void fun(int a:a>200); //#3
    void fun(int a:a<20&&a>10); //#4
    void fun(int a:(a>70&&a<90)||(a<-10)); //#5
    ...
    int a=0, b=15, c=250, d=-50;
    fun(80); //使用#5
    fun(50); //使用#1
    fun(a); //使用#2
    fun(b); //使用#4
    fun(c); //使用#3
    fun(d); //使用#5
    实际上,这无非是在参数声明之后加上一组约束条件,用以表明该版本函数的选择条件。没有约束的函数版本在没有任何约束条件匹配的情况下被选择。对于使用立 即数或者静态对象的调用而言,函数的选择在编译期执行,编译器根据条件直接调用匹配的版本。对于变量作为实参的调用而言,则需要展开,编译器将自动生成如 下代码:
    //首先将函数重新命名,赋予唯一的名称
    void fun_1(int a); //#1
    void fun_2(int a); //#2
    void fun_3(int a); //#3
    void fun_4(int a); //#4
    void fun_5(int a); //#5
    //然后构造分派函数
    void fun_d(int a) {
        if(a==0)
            fun_2(a);
        else if(a>200)
            fun_3(a);
        ...
        else
            fun_1(a);
    }
    在某些情况下,可能需要对一个对象的成员做出约束,此时便可以采用这种形式:
    struct A
    {
        float x;
    };
    ...
    void fun(A a:a.x>39.7);
    ...
    这种施加在类型上的所谓“特化”实际上只是一种语法糖,只是由编译器自动生成了分派函数而已。这个机制在Haskell等语言中早已存在,并且在使用上带 来很大的灵活性。如果没有这种机制,那么一旦需要增加函数分派条件,那么必须手工修改分派函数。如果这些函数,包括分派函数,是第三方提供的代码,那么修 改将是很麻烦的事。而一旦拥有了这种机制,那么只需添加一个相应的函数重载即可。
    当concept-类型重载和类型-对象重载混合在一起时,便体现更大的作用:
    void fun(anytype a);
    void fun(Integers a);
    void fun(Floats a);
    void fun(long a);
    void fun(int a);
    void fun(double a);
    void fun(double a:a==0.8);
    void fun(short a:a<10);
    void fun(string a:a=="abc");
    ...
    concept-类型-对象重载体系遵循一个原则:优先选择匹配的函数中最特化的。这实际上是类型重载规则的扩展。大的来说,所有类型比所属的 concept更加特化,所有对象约束比所属的类型更加特化。对于concept而言,如果concept A refine自concept B,那么A比B更加特化。同样,如果一个类型的约束强于另一个,那么前一个就比后一个更加特化,比如a==20比a>10更加特化。综合起来,可以 有这样一个抽象的规则:两个约束(concept,或者施加在对象上的约束)A和B,作用在类型或者对象上分别产生集合,如果A产生的集合是B产生的集合 的真子集,那么便认为A比B更加特化。
    根据这些规则,实际上可以对一个函数的重载构造出一个“特化树”:
File?id=dd4fc9w_43c5sh64dd_b
    越接近树的根部,越泛化,越接近叶子,越特化。调用时使用的实参便在这棵“特化树”上搜索,找到最匹配的函数版本。
    concept-类型-对象体系将泛型、类型和对象统一在一个系统中,使得函数的重载(特化)具有更简单的形式和规则。并且,这个体系同样可以很好地在类模板上使用,简化模板的定义和使用。

类模板

    C++的类模板特化形式并不惹人喜爱:
    template<typename T> A{...}; //基础模板
    template<> A<int>{...}; //显式特化(全特化)
    template<typename T> A<T*>{...}; //局部特化
    在C++09中,可以直接用concept定义模板的类型形参:
    template<Integers T> A{...};
    实质上,这种形式本身就是一种局部特化,因而原本那种累赘局部特化形式可以废除,代之以concept风格的形式:
    template<Pointer T> A{...}; //Pointer表示此处采用指针特化模板
    同样,如果推广到全特化,形式也就进一步简单了:
    template<int> A{...}; //这个形式有些突兀,这里只打算表达这个意思,应该有更“和谐”的形式
    如果模板参数是对象,则使用现有的定义形式:
    template<int a> A{...};
    更进一步,可以引入对象的约束:
    template<int a:a>10> A{...};
    此外,C++中在模板特化之前需要有基础模板。但实际上这是多余的,D语言已经取消了这个限制,这对于简化模板的使用有着莫大的帮助。

从本质上讲...

    从本质上讲,我们可以把所有类型看作一个集合T={ti},而concept则是施加在类型集合上的约束。通过concept这个约束,我们便可以获得类 型集合T的一个子集C。理论上,所有concept所对应的类型子集Cj构成了类型集合的幂集{Cj}。在{Cj}中,有两类类型子集是很特殊的。一组是 T本 身,即所有类型。存在一个concept不对T施加任何约束,便得到了C0=T。第二类则是另一个极端,存在一组concept,施加在T上之后所得的类 型子集仅包含一个类型:Ci={ti}。由于这组concept与类型存在一一对应的关系,那么我们便可以用这组concept来指代类型。也就是把类型 作为特殊的concept处理。如此,concept便同类型统一在一个体系中。这种处理可以使我们获得极大的好处。
    这组特殊的concept仍旧使用对应的类型名作为称谓,仍旧称之为“类型”,但其本质上还是concept。任何一个类型,一旦创建,也就创建了相应的特殊concept。如果在模板特化中使用一个类型的时候,实际上就是在使用相对应的那个特殊concept:
    void func(typeA a); //尽管使用了类型名typeA,但实际上这里所指的是typeA所对应的那个特殊concept。
    在这个concept体系的作用下,函数模板的特化和重载整个地统一起来(concept based overload)。
    至于作用在类型上的那种“特化”,也是同样的道理。对于一个类型T而言,它所有的对象构成一个集合O。如果存在一组约束作用于O,那么每 一个约束对应着O的一个子集。理论上,我们可以构造出一组约束,使得他们同O的每一个子集一一对应。同样,这些子集中有两类子集比较特殊。一类是所有对象 的集合。另一类便是只有一个对象的子集。于是,我们可以使用这组特殊对象子集所对应的约束指代相应的对象。也就是将对象看作特殊的约束。如此,类型和对象 也被统一在一个系统中了。
    进而,类型在逻辑上被作为特殊concept处理,对象则被作为特殊的类型处理。于是,这三者便可以统一在一个体系下,一同参与特化。

总结

    尽管形式不能代表本质,但形式的变化往往会带来很多有益的进步。更重要的是,很多本质上的变化总会伴随着形式上的改变。通过将concept、类型和对象 在逻辑上整合到统一的体系之中,便可以促使模板、特化、函数重载等机制在形式上达成统一。从而能够简化这些功能的使用。这也是当前重视语言(工具)易用性 的潮流的一个必然诉求。这个形式上的统一并非语法糖之类的表面变化。而是完全依赖于concept这个新型的类型描述(泛型)系统的确立和发展。 concept的出现,弥补了以往泛型的不足,找回了泛型系统缺失的环节,弥补了泛型同类型之间的裂痕。在此作用下,便可以构建起concept-类型- 对象的抽象体系,用统一的系统囊括这三个原本分立的概念。在这个新的三位一体的系统下,使得模板的特化和重载拥有了相同的形式,进而获得更直观的语义,和 更好的易用性。
展开阅读全文

GP技术展望——C--

08-02

声明一下,论坛编辑器吞没了我所有的链接,我不想再重新加了,太多了。如果想要看链接的,请移步我的blog:http://blog.csdn.net/longshanksrnrnrn[size=16px][align=center][b]GP技术的展望——C--[/b][/align][/size]rn[align=center]莫华枫[/align]rnrn C++的复杂是公认的,尽管我认为在人类的聪明智慧之下,这点复杂压根儿算不上什么。不过我得承认,对于一般的应用而言,C++对程序员产生的压力还是不小的。毕竟现在有更多更合适的选择。仅仅承认复杂,这没有什么意义。我不时地产生一个念头:有什么办法既保留C++的优点,而消除它的缺点和复杂。我知道 D语言在做这样的事情。但是,D更多地是在就事论事地消除C++的缺陷,而没有在根本上消除缺陷和复杂性。rn 一般而言,一样东西复杂了,基本上都是因为东西太多。很显然,C++的语言特性在众多语言中是数一数二的。于是,我便想到或许把C++变成“C--”,可以治好C++的复杂之病。在探讨这个问题之前,让我们先从C出发,看看C++为何如此复杂。rnrn[b]C和C++[/b]rn 尽管存在这样那样的不足,比如non-lalr的语法、隐式的指针类型转换等,但C语言的设计哲学却是足够经典的。C语言有一个非正式的分类,认为它既非汇编这样的低级语言,也非Pascal那样的高级语言,而应该算作中级语言,介于其他两类语言之间。这种分类恰如其分地点出了C语言的特点和理念:以高级语言语法形式承载了低级语言的编程模型。低级语言的特点是可以直接地描述硬件系统的结构。C则继承了这个特点。C语言直观地反映了硬件的逻辑构造,比如数组是内存块,可以等价于指针。在C语言中,我们可以几乎直接看到硬件的构造,并且加以操作。这些特性对于底层开发至关重要。rn 然而,C的这种直观简洁的模型过于底层和琐碎,不利于应用在那些构造复杂、变化多样的应用中。针对C的这些弱点,Bjarne Stroustrup决心利用OOP技术对C语言进行改造,从而促使了C++的诞生。C++全面(几乎100%)地兼容C,试图以此在不损失C语言的直观和简洁的情况下,同时具备更强的软件工程特性,使其具备开发大型复杂系统的优势。这个目标“几乎”达到了,但是代价颇为可观。rn 在经历了80、90年代的辉煌之后,C++的应用领域开始退步。一方面,在底层应用方面,C++的很多特性被认为是多余的。如果不使用这些特性,那么 C++则同C没有什么差别。相反这些特性的使用,对开发团队的整体能力提出了更高的要求。因而,在最底层,很多人放弃了C++而回归了C,因为那些高级特性并未带来很多帮助,反而产生了很多负担。另一方面,在高层开发中,业务逻辑和界面也无需那么多底层的特性和苛刻的性能要求,更多简单方便、上手容易的语言相比C++更加适合。C++的应用被压缩在中间层,随着业界系统级开发的不断专业化,C++的使用规模也会越来越小。(当然,它所开发的应用往往都是关键性的,并且是没有选择余地的)。实际上,C++在这个层面也并非完美的工具。目前无法取代是因为没有相同级别的替代品。D或许是个强有力的竞争者,但一方面出于历史遗留代码的规模和应用惯性,另一方面D也并未完全解决C++面临的复杂性问题,D也很难在可见的将来取代C++。rn 实际上,C++的这种尴尬地位有着更深层次的原因。C++的本意是在保留C的底层特性基础上,增加更好的软件工程特性。但是,C++事实上并未真正意义上地保留C的底层特性。回顾一下C的设计理念——直观而简洁地反映底层硬件的特性。C++通过兼容C获得了这种能力。但是这里有个问题,如果我要获得C的这种简单直观性,就必须放弃C++中的很多高级特性。这里最明显的一个例子便是pod(plain old data)。rn 在C中压根没有pod的概念,因为所有的对象都是pod。但是,C++中有了pod。因为C++的对象可能不是一个pod,那么我们便无法象在C当中那样获得直观简洁的内存模型。对于pod,我们可以通过对象的基地址和数据成员的偏移量获得数据成员的地址,或者反过来。但在非pod对象中,却无法这么做。因为C++的标准并未对非pod对象的内存布局作出定义,因而对于不同的编译器,对象布局是不同的。而在C中,仅仅会因为底层硬件系统的差异而影响对象布局。rn 这个问题通常并不显而易见。但在很多情况下为我们制造了不小的障碍。比如,对象的序列化:我们试图将一个对象以二进制流的形式保存在磁盘中、数据库中,或者在网上传输,如果是pod,则直接将对象从基地址开始,按对象的大小复制出来,或传输,或存储,非常方便。但如果是非pod,由于对象的不同部分可能存在于不同的地方,因而无法直接复制,只能通过手工加入序列化操作代码,侵入式地读取对象数据。(这个问题不仅仅存在于C++,其他语言,如java、C# 等都存在。只是它们没有很强烈的性能要求,可以使用诸如reflect等手段加以处理)。同样的问题也存在于诸如hash值的计算等方面。这对很多开发工作造成不小的影响,不仅仅在底层,也包括很多高层的应用。rn 究其原因,C++仅仅试图通过机械地将C的底层特性和OOP等高层特性混合在一起,意图达到两方兼顾的目的。但是,事与愿违,OOP的引入实际上使得C的编程模型和其他更高级的抽象模型无法兼容。在使用C++的过程中,要么只使用C的特性,而无法获得代码抽象和安全性方面的好处,要么放弃C的直观简洁,而获得高层次的抽象能力。反而,由于C和OOP编程模型之间的矛盾,大大增加了语言的复杂性和缺陷数。rnrn[b]舍弃[/b]rn 但是,我们可以看到在C++中,并非所有的高级特性都与C的底层特性相冲突。很多使用C而不喜欢C++的人都表示过他们原意接受OB,也就是仅仅使用封装 。对于RAII,基本上也持肯定的态度。或许也会接受继承,但也表露出对这种技术带来的复杂性的担心。动多态是明显受到排斥的技术。显然这是因为动多态破坏了C的编程模型,使得很多本来简单的问题复杂化。不是它不好,或者没用,是它打破了太多的东西。rn 因而,我们设想一下,如果我们去除动多态特性,那么是否会消除这类问题呢?我们一步步看。rn 动多态的一个基本支撑技术是虚函数。在使用虚函数的情况下,类的每一次继承都会产生一个虚函数表(vtable),其中存放的是指向虚函数的指针。这些虚函数表必须存放在对象体中,也就是和对象的数据存放在一起(至少要关联在一起)。因而,对象在内存里并不是以连续的方式存放,而被分割成不同的部分,甚至身首异处(详见《Inside C++ Object Model》)。这便造成了前面所说的非pod麻烦。一旦放弃虚函数和vtable,对象的内存布局中,便不会有东西将对象分割开。所有的对象的数据存储都是连续的,因而都是pod。在这一点上,通过去除vtable,使得语言回归了C的直观和简单。rn 动多态的内容当然不仅仅是一个虚函数,另一个重要的基石是继承。当然,我们并不打算放弃继承,因为它并不直接破坏C的直观性和简洁性。不同于虚函数,继承不是完全为了动多态而生的。继承最初的用途在于代码复用。当它被赋予了多态含义后,才会成为动多态的基础。以下的代码可以有两种不同的解读:rn class B : public A ;rn 从代码复用的角度来看,B继承自A,表示我打算让B复用A的所有代码,并且增加其他功能。而从多态的角度来看,B是一个A的扩展,B和A之间存在is-a的关系。(B是一个A)。两者是站在不同层面看待同一个问题。代码复用,代表了编码的观点,而多态,则代表了业务逻辑的观点。但是,两者并非实质上的一回事。在很多情况下,基类往往作为继承类的某种代表,或者接口,这在编码角度来看并没有对等的理解。而这种接口作用,则是动多态的基础。动多态通过不同的类继承自同一个基类,使它们拥有共同的接口,从而可以使用统一的形式加以操作。作为一个极端,interface(或者说抽象基类),仅仅拥有接口函数(即vtable)而不包含任何数据成员。这是纯粹的接口。rn 然而,这里存在一个缺陷。一个接口所代表的是一组类,它将成为这一组类同外界交互的共同界面。但是,使用基类、或者抽象基类作为接口,实质上是在使用一个类型来代表一组类型。打个比方,一群人凑在一起出去旅游,我们称他们这群人为“旅行团”。我们知道旅行团不是一个人,而是一个不同于“人”的概念。动多态里的接口相当于把一个旅行团当作一个人来看待。尽管这只是逻辑上的,或许一个旅行团的很多行为和一个人颇为相似。但是根本上而言,两者毕竟不是相同层次的概念。这样的处理方法往往会带来了很多弊端。rn 为了使继承被赋予的这重作用发挥作用,还需要一项非常关键的处理:类型转换。请看以下代码:rn void func(A* a);rn B b;rn func(&b);rn 最后这行代码施行了动多态,如果B override了A的虚函数的话。很显然,如果严格地从强类型角度而言,&b是不应当作为func的实参,因为两者类型不匹配。但是如果拒绝接受&b作为实参,那么动多态将无法进行下去。因此,我们放宽了类型转换的限制:允许继承类对象的引用或指针隐式地转换成基类的引用或指针。这样,形如func(&b);便可以顺理成章地成为合法的代码。rn 然而,这也是有代价的:rn B ba[5];rn func(ba);rn 后面这行函数调用实际上是一个极其危险的错误。假设在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++中的一个重要的陷阱——对象切割。这种错误相当隐蔽,危险性极大。rn 由于C++试图保留C的编程模型,因而保留了指针-数组的等价性。这种等价性体现了数组的本质。这在C中是一项利器,并无任何问题。但在C++中,由于存在了继承,以及继承类的隐式类型转换,使得这种原本滋补的特性成为了一剂毒药。换句话说,C++所引入的动多态破坏了C的直观性。rnrn[b]舍弃之后[/b]rn 从上面的分析来看,动多态同C的编程模型是不相容的。因而如果希望得到C的直观性,并且消除C++的缺陷,必须放弃动多态这个特性。现在来看看放弃之后将会怎样。rn 一旦放弃了动多态,也就放弃了虚函数和vtable。此时,所有的对象都是pod了。那么首当其冲的好处,就是可以进行非侵入的序列化、hash计算等等操作。由于对象肯定是连续分布的,可以直接地将对象取出进行编码、存储、计算和传输,而无需了解对象内部的数据结构和含义。另外一个重要的问题也会得到解决,这就是ABI。在C中统一的ABI很自然地存在于语言中。我们可以很容易地用link将两个不同编译器编译的模块连接起来,而不会发生问题。但是,C++中做不到,除非不再使用类而使用纯C。目前C++还没有统一的ABI,即便标准委员会有意建立这样的规范,实现起来也绝非易事。但是,如果放弃动多态之后,对象的布局便回归到C的形态,从而使得ABI不再成为一个问题。rn 另一方面,随着动多态的取消,那么继承的作用被仅仅局限于代码复用,不再具有构造接口的作用。我们前面已经看到,继承类向基类的隐式转换,是为了使基类能够顺利地成为继承类的接口。既然放弃了动多态,那么也就无需基类再承担接口的任务。那么由继承类向基类的隐式类型转换也可以被禁止:rn void func(A* a);rn B b;rn func(&b); //编译错误,类型不匹配rn 进而对象切割也不会发生:rn B ba[5];rn func(ba); //编译错误,类型不匹配rn 尽管根据数组-指针的等价性,ba可以被隐式地转换为B*,但是B*不再能够隐式地转换为A*,从而避免了对象的切割。rn 问题是,如此简单地将动多态放弃掉,就如同将水和孩子一起泼掉那样,实际上放弃了动多态带来的好处。实际上并非如此。我们放弃动多态这个特性,但并不打算放弃它所具有的功能,而是用另一种技术加以替代。这便是runtime concept(这里和这里)。rn 不同于以类型为基础的interface,concept是独立于类型的系统。concept生来便是为了描述一组类型,因而是接口最理想的实现手段。当concept runtime化之后,便具有了与动多态相同的功能(很多方面还有所超越)。rn runtime concept同样需要类似vtable的函数分派表,但由于它不是类型,这些分派表无需存放在对象内部,可以独立放置(可以同RTTI信息放在一起),并且只需一份。正是基于这个特性,方才保证了所有对象依然是pod,依然能够保证对象布局的直观性。rn 同样,runtime concept承担了接口的任务,但不象动多态那样依赖于继承和相应的隐式类型转换。(通过自动或手动的concept_map)。因而,我们依旧可以禁止基于继承关系的隐式类型转换,从而防止对象切割的情况。rn 一旦使用concept作为多态的实现手段,反倒促使原本动多态的一些麻烦得到消除。在动多态中,必须指定virtual函数。如此,在一个类中会存在两种不同形态的函数,实现动多态的虚函数,和无此功能的普通函数。准确地维护这样两种函数,颇有些难度。而且,函数是虚还是不虚,牵涉到系统的设计,必须在最初构建时确定,否则以后很难修改。但在放弃动多态,使用concept的情况下,只要一个继承类中,使用相同的签名覆盖基类中的函数,便实现了多态。当进行concept_map,即将接口与类绑定时,只会考虑继承类的函数,而忽略基类中被覆盖的函数。于是,只需简单的覆盖,便实现了多态的控制。对于是否多态一个函数,即是否改变基类函数的行为,完全由继承类控制,在创建基类时不必为此伤神。其结果就是,我们无需在系统设计的最初一刻就操心多态的问题,而只需根据实现的需要随时实现。rnrn[align=center]其他[/align]rn 存在大量隐式转换也是C++常受人诟病的一个方面,(特别是那些Pascal系的程序员)。隐式转换的目的是带来方便,使得编码更加简洁,减少冗余。同时也使得一些技巧得以施行。但是,隐式转换的副作用也颇为可观。比如:rn void fun(short a);rn long a=1248;rn fun(a); //顶多一个警告rn 这种转换存在两面性:一方面,它可能是合理的,因为尽管a类型long大于short,但很可能存放着short可容纳的数值;但另一方面,a的确存在short无法容纳的可能性,这便会造成一个非常隐蔽的bug。rn C/C++对此的策略是把问题扔给程序员处理,如果有bug那是程序员的问题。这也算得上合情合理,毕竟有所得必有所失,也符合C/C++的一贯理念。但终究不是最理想的方式。但是如果象Pascal那样将类型管得很死,那么语言又会失去灵活性,使得开发的复杂性增加。rn 如果试图禁止隐式类型转换,那么为了维持函数使用代码的简洁性,函数必须对所有的类型执行重载。这大大增加了函数实现的负担,并且重复的代码严重违背了DRY原则。rn 现在或许存在一些途径,使得在维持绝对强类型的情况下获得所希望的灵活性。钥匙可能依然在concept手上。考虑如下的代码:rn void fun(Integers a);rn long a=1248;rn fun(a);rn longlong b=7243218743012;rn fun(b);rn 此处,fun()是一个函数,它的形参是一个concept,代表了所有的整型。这样,这个函数便可以接受任何一种整型(或者具有整型行为的类型)。我们相信,在一般的应用下,任何整数都有完全相同的行为。因此,我们便可以很方便地使用Integers这个接口执行对整数的操作,而无需关心到底是什么样的整数。rn 如此,我们便可以在禁止隐式类型转换,不使用函数重载的情况下,完成这种函数的编写。同时可以得到更好的类型安全性。rnrn 强制类型转换是非常重要的特性,特别是在底层开发时。但也是双刃剑,往往引来很隐蔽的错误。强制类型转换很多情况下是无理的,通常都是软件的设计问题造成的。但终究还是有一些情况,需要它来处理。rn 设想这样一个场景:两个一模一样的类型,但它们分属不同的函数。(这种情形尽管不多见,但还是存在的。这往往是混乱设计的结果。当然也有合理的情况,比如来自两个不同库的类型)。我现在需要写一个函数,能够同时使用这两个类型。比较安全一些的,可以用函数重载。但是两个重载的函数代码是一样的,典型的冗余代码。当然也可以针对其中一个结构编写代码,然后在使用时,对另一个结构的实例执行强制类型转换。但是,强制类型转换毕竟不是件好事。因此,我们也可以构造一个concept,让它描述这两个类型。然后在编写函数时使用这个concept,当这两个类型都与concept绑定后,便可以直接使用这两个类型,而没有类型安全和代码冗余的问题。rn (顺便提一下,这种方式也可以运用在类型不同的情况下。比如两个类型不完全相同,但是基本要素都一样。那么就可以使用concept_map的适配功能,将两个类型统一在一个concept下。这种方式相比oop的Adapter模式,更加简洁。adapter本身是一个container,它所实现的接口函数,都必须一一转发到内部的对象,编写起来相当繁琐。但在concept_map中,对于那些符合concept描述的函数无需另行处理,concept会自动匹配,只需对那些不符合要求的函数执行适配。) 论坛

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 论坛

先有鸡还是先有蛋?先有操作系统,还是先有汇编器?(对计算机本质的探讨 3)

08-02

这个是以前给别人的回帖,感觉对初学者应该还有点帮助吧,于是就无耻的再贴出来了,大家包涵。rnrn内容开始:rnrn“这个问题很好,说明搂主是个寻根究底的人,任何求知欲强的人都会通过探索层层深入,最后到最根本的问题。最痛恨一知半解的人乱回答别人的问题,或者找风马牛不相及的答案糊弄人!这样的人就别搞技术了,去搞政治更合适!rnrn你的困惑感应该和蛋鸡问题类似,到底是先有操作系统还是先有汇编器?其实只要对计算机史了解的话就不会迷惑了。原始的电子计算机是没有操作系统也没有汇编器的,那时编程是靠写一条条机器指令(就是高低电位组成的信号)进行的,没多久,出现了汇编语言,就是用符号来表示原本的指令由程序员写出这些符号指令,再由其他人对表翻译成真正的机器指令。这就是所谓的手工汇编。后来出现了用程序来代替人的查表,就是现在的机器汇编,从此现在意义上的汇编语言诞生。rnrn运行汇编程序并不一定要操作系统,只要把程序加载到内存,然后cpu逐条执行就可以了,而操作系统也未必非要汇编语言来写,也可以用机器指令写的。也就是说他们在逻辑上并非一定的因果关系。所以历史上也没有明确的先后关系,是同时交错进行的。那时没有操作系统的概念,只是一个个工具程序而已。rnrn你的困惑还在于最初程序是如何输入进计算机的,现在要把一个程序加载到内存,需要借助别的程序,那这个“别的程序”又是怎么到内存里的呢?还是蛋鸡问题。其实,最早把程序加入计算机里的方法是搬开关,一位一个开关来代表高低电位。后来打卡片(和纸带),再用转换器读入。再后来出现了磁介质,存储和加载靠程序对磁介质读写来完成,这就不需要借助体力和外部设备了,存储和加载都在计算机内部完成。那么这个“最初的加载程序”当然不是靠程序加载的,是靠人力加载的(可以搬开关,也可以打孔。)rnrn再后来呢,由于输入汇编指令太累,所以有人就开发了一中程序,叫“自动编程器”,就是输入一条高级指令,由那个程序解释成多条机器指令,这就是后来的高级语言。rnrnps:其实计算机的本质就是一条条电信号(机器指令)来推动运行的,和一台内部有很多齿轮的精密大钟没什么两样,都是机器!什么操作系统啦,什么程序啦,都是我们抽象后给他们起的名字,他们本质还是一个个机器指令,注意,我很反对某些人说机器指令就是1、0数字组成的,这很不确切容易使人误解(可能他自己就误解了),应该说是由高低电位组成的,只不过我们把高电位看作1,低电位看作0。我还很鄙视某些缺乏探索精神的所谓的“技术人员”,在他们看来好像在键盘上按一个键,然后显示器上显示出字符,这么一个简单的过程是理所应当的,不值得探索,殊不知就是这个简单的过程也是很多很多内部指令的结果,这些都不思考更不要指望他们去思考“最初的操作系统”类的问题了。一句话,没有探索的欲望和好奇心,就不要搞研究!”rnrnrn结束 论坛

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