concept的外快

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/longshanks/article/details/4547747

concept的外快

    Concept,一个逝去的梦,未来的希望,抽象之毒的解药,...

    Concept!标准委员会叫你回家吃饭!
    我们等待的不是C++标准,是寂寞...
    就此默哀三分钟...

    Concept作为更加完善的抽象体系,消除了OOP等抽象机制的缺陷,将抽象手段提升到一个无忧的境界。与之相关的一个辅助机制,concept map/adapter,则为我们提供了更加优雅的类型扩展之路。这里,我通过一个改编自SICP的案例,来展示其中的奥妙和强劲。必须说明的是,这里所 用到的concept map机制,是被前C++0x(1x?)的concept所禁止的。所以在这里我想吼一声:WG21的死脑筋们,看看你们都干了什么!
    说有两个程序员,一个叫笨,另一个叫爱死理。他们俩各写了一个表达复数的类型:
    //笨的复数类,使用直角坐标表示复数
    class BenComplex
    {
    public:
        //构造函数
        BenComplex(float real, float img){...}    //用实部和虚部创建一个复数
        //访问函数
        float getReal(){...}    //提取实部
        float getImg(){...}    //提取虚部
        ...
    };
    //爱死理的复数类,使用极坐标表示复数
    class AsslyComplex
    {
    public:
        //构造函数
        AsslyComplex(float mag, float angle){...}    //用复数向量的模和幅角创建一个复数
        //访问函数
        float getMag(){...}    //提取模
        float getAngle(){...}    ///提取幅角
    };
    好,现在我们需要对复数执行计算。先看加法。复数的加法使用直角坐标表示法来得容易,因为只需分别将它们的实部和虚部相加即可:
    BenComplex operator+(BenComplex lhd, BenComplex rhd) {
        return BenComplex(lhd.getReal()+rhd.getReal(), lhd.getImg()+rhd.getImg());
    };
    而乘法则使用极坐标表示法来的容易:
    AsslyComplex operator*(AsslyComplex lhd, AsslyComplex rhd) {
        return AsslyComplex(lhd.getMag()*rhd.getMag(), lhd.getAngle()+rhd.getAngle());
    }
    很显然,如果想要让两个极坐标表示的复数(即AsslyComplex的实例)相加,或者两个直角坐标表示的复数(即BenComplex的实例)相 乘,要么另行定义新的operator+,要么对复数做转换。通常,从我们会选择后者,因为这么做拥有抽象上的优势:
    float getReal(BenComplex c){ return c.getReal();}
    float getReal(AsslyComplex c){ return c.getMag()*cos(c.getAngle()); }
    float getImg(BenComplex c){ return c.getImg();}
    float getImg(AsslyComplex c){ return c.getMag()*sin(c.getAngle()); }
    float getMag(BenComplex c){ return sqrt(c.getReal()*c.getReal()+c.getImg()*c.getImg());}
    float getMag(AsslyComplex c){ return c.getMag(); }
    float getAngle(BenComplex c){ return arctan(c.getImg()/c.getReal());}
    float getAngle(AsslyComplex c){ return c.getAngle(); }
    然后,加法和乘法的代码便成为:
    template<typename T>
    T operator+(T lhd, T rhd) {
        return T(getReal(lhd) +getReal(rhd) , getImg(lhd) +getImg(rhd) );
    };
    template<typename T>
    T operator*(T lhd, T rhd) {
        return T(getMag(lhd) *getMag(rhd) , getAngle(lhd) +getAngle(rhd) );
    }
    非常简洁,非常抽象,并且充满了了对称之美。但是,非常累人。两个复数类,四种访问,需要写8个函数。而且其中一半仅仅是做了一个简单的调用,实在有些浪费。如果有更多的复数表达类,那么会更加累人,更加浪费。
    看到这里,那些顶破蛋壳,第一眼看到的就是OOP的程序员笑了:用OOP的接口,就不会那么浪费了:
    class IRectComplex
    {
        float getReal()=0;
        float getImg()=0;
    }
    class IPolarComplex
    {
        float getMag()=0;
        float getAngle()=0;
    }
    class BenComplex
        : public IRectComplex, IPolarComplex
    {
        ...
        //这两个就是原来的
        float getReal(){...}
        float getImg(){...}
        //这是新加的,为了极坐标
        float getMag(){
            return sqrt(getReal()*getReal()+getImg()*getImg());
        }
        float getAngle(){
            return arctan(getImg()/getReal());
        }
    };
    class AsslyComplex
        : public IRectComplex, IPolarComplex
    {
        ...
        //这两个就是原来的
        float getMag(){...}
        float getAngle(){...}
        //这是新加的,为了直角坐标
        float getReal(){
            return c.getMag()*cos(c.getAngle());
        }
        float getImg(){
            return c.getMag()*sin(c.getAngle());
        }
    };
    BenComplex operator+(IRectComplex lhd, IRectComplex rhd) {
        return BenComplex(lhd.getReal() +rhd.getReal() , lhd.getImg() +rhd.getImg() );
    }
    AsslyComplex operator*(IPolarComplex lhd, IPolarComplex rhd) {
        return AsslyComplex(lhd.getMag() *rhd.getMag() , lhd.getAngle() +rhd.getAngle() );
    }
    两个接口,IRectComplex和IPolarComplex,分表描述直角坐标和极坐标所需的函数。而两个类都实现这两个接口。这样,无论哪个类都可以直接用于+和*操作,而无需一个额外的转换。同时,也减少了那一半没有做任何计算的函数。
    但是,OOPer们,别高兴得太早,麻烦接踵而至。首先,笨和爱死理不高兴了。
    笨说:“我写的复数类用的是直角坐标,干嘛还要实现一个极坐标的接口,我就是不喜欢极坐标,太恶心人了...”
    而爱死理说:“我就是喜欢极坐标,优雅!干嘛还非得实现一个直角坐标接口,我讨厌直角坐标,呆板...”
    然后,还有件更恐怖的事:一个新来的程序员,用了一个新的复数表示法。这样,便有了三个接口需要每一个类实现。笨和爱死理无论如何不肯再加接口了。于是,三个人吵成一团。
    很显然,OOP的Interface(抽象类)是侵入式的,必须由相关类型配合,加以实现。如果开发者不配合,或者无法配合,那么事情就混乱了。而且,对于变化的适应性不如前面的转换函数来的强,也不够灵活。
    小结一下,OOP方式,可以减少很多无意义的代码,但面对变化的灵活性差。转换函数(也可以看作另一种形式的接口)的方式,灵活性高,但需要多写很多没有执行实际转换的函数。两者互有胜负,各有优缺点。
    接下来,我打算通过concept/concept map/adapter,实现一匹不吃草的好马儿。
    首先,我们接受笨和爱死理最初开发的那两个类,笨的类只考虑直角坐标的东西,而爱死理的类只考虑极坐标的操作。两者不用实现对方的接口,互不搭界。
    然后,我们定义两个concept。(请注意,这些事情我们都是在笨和爱死理不知情的情况下做的,以免他们不开心):
    concept RectComplex<T>
    {
        float T::getReal();
        float T::getImg();
    };
    concept PolarComplex<T>
    {
        float T::getMag();
        float T::getAngle();
    };
    接着,我们可以编写+和*操作了:
    BenComplex operator+(RectComplex lhd, RectComplex rhd) {
        return BenComplex(lhd.getReal()+rhd.getReal(), lhd.getImg()+rhd.getImg());
    }
    AsslyComplex operator*(PolarComplex lhd, PolarComplex rhd) {
        return AsslyComplex(lhd.getMag()*rhd.getMag(), lhd.getAngle()+rhd.getAngle());
    }
    最后,在用之前,我们必须将类型和concept绑定。但是,这不是一般的绑定,绑定的同时,我们还需要弥合各种不同的复数表示法之间的差异:
    concept_map RectComplex<BenComplex>;
    concept_map PolarComplex<AsslyComplex>;
    这两个绑定是顺理成章的,BenComplex本来就是直角坐标表示法,而AsslyComplex本来就是极坐标表示,它们与相应的concept之间完全契合,无须额外修正。接下来,需要面对不同表示法之间的绑定了:
    concept_map RectComplex<AsslyComplex>
    {
        float AsslyComplex::getReal() {
             return that .getMag()*cos(that .getAngle());
        }
        float AsslyComplex::getImg() {
            return that .getMag()*sin(that .getAngle());
        }
    };

    concept_map PolarComplex<BenComplex>
    {
        float BenComplex::getMag() {
            return sqrt(that .getReal()*that .getReal()+that .getImg()*that .getImg());
        }
        float BenComplex::getAngle() {
             return arctan(that .getImg()/that .getReal());
        }
    };
    这些代码做了两件事。一是将两个复数类和concept绑定;第二是“制造”出concept有,而复数类没有的成员函数。后者是这里的要点。 concept map俨然成了一个adapter,将一个类型“打扮”成concept所需的样子。关键字that与this相对,this用于对类的内部的访问,而 that则是从外部访问一个对象。但这种concept map在C++0x的是被禁止的。在C++0x中,concept map的adapter不能作用于成员函数。理由是不能破坏类的封装。但是,如果允许adapter函数,如上面的 BenComplex::getMag(),能够访问类BenComplex的non-public成员,的确会破坏封装。但是如果我们只允许访问 BenComplex的public成员,那么便不会有此问题。这也就是that的意义:this访问类的内部,而that访问类的外部。
    这相当于为一个类添加了额外的成员。C#程序员可能会觉得眼熟。没错,类似extension method。但concept map有它的优势。extension method的作用范围是全局的,会“污染”所有涉及的类型。而concept map的作用范围仅仅局限在相应的concept之中。一个类型在任何地方都会保持其原本的形象,忠实体现设计者的意图。只有当我们通过concept访 问一个类型时,这些“附加”的成员才会起作用。而这些附加的成员也完全是concept的需求,不会有任何突兀和随意。
    但是,此处还有一个问题。+和*的代码中,使用了具体的类型BenComplex和AsslyComplex作为返回类型。这直接导致了两个操作同这两个 具体类型的依赖。我们不希望一个通用性的操作同一个具体类型相关,因为不利于提高抽象度。解决的方法有这样几种:
    最简单的,使用在算法中使用类型别名,而非具体类型:

    ComplexPlusRet operator+(RectComplex lhd, RectComplex rhd) {
        return ComplexPlusRet (lhd.getReal()+rhd.getReal(), lhd.getImg()+rhd.getImg());
    }
    在导入+操作前(include),定义ComplexPlusRet即可。这种方案尽管简单,但只是提供了一个间接,并未彻底消除两者的依赖关系。
    稍微复杂些的,就是将这个类型别名的定义放入concept:
    concept RectComplex
    {
        typedef BenComplex RetType;
        ...
    };
    RectComplex::RetType operator+(RectComplex lhd, RectComplex rhd) {
        return RectComplex::RetType (lhd.getReal()+rhd.getReal(), lhd.getImg()+rhd.getImg());
    }
    这种方式更方便些,但同样也没有彻底消除依赖。因为concept是算法的接口,属于算法的一部分,它的某个成分依赖于具体类型,那么也就是这个算法依赖于那个类型了。
    于是,我们可以考虑将类型别名定义进一步推迟到concept map的时候:
    concept RectComplex
    {
        typedef RetType;     //concept中的声明,占位
    };
    ...
    concept_map RectComplex<BenComplex>
    {
        typedef BenComplex RetType;     //实际的定义,绑定
    };
    concept_map RectComplex<AsslyComplex>
    {
        typedef BenComplex RetType;
        ...
    };
    ...
    如此,类型的定义同操作的定义彻底分离,两者可以独立开发,互不相关。只有在使用类型和操作的时候,才需要使用者将类型同concept绑定。
    但是,这个方案也并非完美的:这里只定义了一个类型别名,但事实上,所需的类型别名还有很多,比如*操作的返回类型就不同于+操作的返回类型,需要 AddRetType和MulRetType。每一个这样的类型别名都需要独立命名和定义。这带来了类型别名的组合爆炸,增加了开发负担,破坏了抽象。
    解决此问题的线索蕴藏在SICP这本经典中。SICP谈到了数据的本质。归纳起来,数据的本质就是数据的特征,而不是数据的实现。比如复数,只要一个类型 满足以下条件,便可以认为它是一个复数(或者当作复数使用。数学上的复数还有更复杂的定义,这里仅仅从编程的数据类型角度出发):
    1、拥有创建的操作,需要两个实数作为参数,分别表示实部和虚部。
    2、有提取实部的操作。
    3、有提取虚部的操作。
    这个定义实际上描述了一类数据类型,也就是接口的描述。concept作为接口,应当满足这些要求才是。我们回过头看前面的RectComplex和PolarComplex。它们与这个复数的定义相比,少了关键性的东西:创建操作。
    BenComplex和AsslyComplex各自有构造函数,可以创建对象。但是,当我们将一个AsslyComplex的对象作为+的参数时,不能直接使用它的构造函数:
    typeof(lhd) operator+(RectComplex lhd, RectComplex rhd) {
        return typeof(lhd) (lhd.getReal()+rhd.getReal(), lhd.getImg()+rhd.getImg());
    }
    AsslyComplex x, y, z;
    z=x+y;
    这样的代码是错误的。此时,lhd的类型,即typeof(lhd),是AsslyComplex。使用它的构造函数,会将原来的实部/虚部作为模/幅角创建AsslyComplex对象,产生错误的结果。
    现在,我们在两个concept中增加创建函数,也可以认为是concept的“构造函数”:
    concept RectComplex<T>
    {
        RectComplex(float real, float img) ;
        float T::getReal();
        float T::getImg();
    };
    concept PolarComplex<T>
    {
        PolarComplex(float mag, float angle);
        float T::getMag();
        float T::getAngle();
    };
    concept map也有相应的变化:
    concept_map RectComplex<BenComplex>;    //BenComplex的构造函数符合RectComplex中对于构造函数的要求,直接使用类型的构造函数。
    concept_map PolarComplex<AsslyComplex>;    //AsslyComplex的构造函数符合PolarComplex中对于构造函数的要求,直接使用类型的构造函数。
    concept_map RectComplex<AsslyComplex>
    {
        RectComplex(float real, float img) {
            AsslyComplex(sqrt(real*real, img*img), arctan(img/real));
        }
        float AsslyComplex::getReal() {
             return that .getMag()*cos(that .getAngle());
        }
        float AsslyComplex::getImg() {
            return that .getMag()*sin(that .getAngle());
        }
    };
    concept_map PolarComplex<BenComplex>
    {
        PolarComplex(float mag, float angle) {
            BenComplex(mag*cos(angle), mag*sin(angle))
        }
        float BenComplex::getMag() {
            return sqrt(that .getReal()*that .getReal()+that .getImg()*that .getImg());
        }
        float BenComplex::getAngle() {
             return arctan(that .getImg()/that .getReal());
        }
    };
    concept增加了“构造函数”,完善了对复数特征的描述。而concept map进一步对于无法满足接口要求的复数类的创建操作构建了adapter。那么如何才能调用这些adapt之后的“构造函数”呢?
    RectComplex operator+(RectComplex lhd, RectComplex rhd) {
        return RectComplex<typeof(lhd)> (lhd.getReal()+rhd.getReal(), lhd.getImg()+rhd.getImg());
    }
    PolarComplex operator*(PolarComplex lhd, PolarComplex rhd) {
        return PolarComplex<typeof(lhd)> (lhd.getMag()*rhd.getMag(), lhd.getAngle()+rhd.getAngle());
    }
    代码中的粗体部分就是答案。typeof(lhd)提取出参数的类型,比如AsslyComplex,用 RectComplex<AsslyComplex>这样的语法调用adapter构造函数。它的含义是在RectComplex接口的控制 下,创建AsslyComplex对象。编译器看到RectComplex<AsslyComplex>的语句,直接到 concept_map中寻找相同的定义,调用相应的构造函数。
    此处很明显地体现出concept优于oop interface的地方。除了非侵入外,concept可以描述构造函数,而interface则无此功能。因此,interface无法完整地描述一 个类型的特征,而concept拥有更加完善的描述能力。此外,由于oop利用继承作为类型与interface绑定的途径。而继承的原始意图并非于此, 担当此任属于“玩票”,因而缺乏进一步扩展的能力。相比之下,concept map则是天生的绑定机制,拥有更大的空间执行adapter之类的任务,具有更大的灵活性和拓展性。这一点在前面的案例中已经充分体现。
    
    至此,我们利用concept,使得抽象算法的同具体类型的开发分离,做到完全无关,并在需要时利用concept map将两者结合。concept的非侵入特性的优势在此处显露无疑。两个Complex类的作者笨和爱死理对于+和*操作的实现一无所知,他们只管按各 自喜欢的表达法实现相应的复数类,不需要考虑其他问题。而两个操作的开发也无须考虑复数类的具体实现,所关心的是最方便的算法和接口。两者之间的桥梁是 concept map和adapter,同时提供了灵活和简洁。它具备了转换函数的灵活性和扩展性,同时又具备了OOP接口的简洁和方便。在concept map和adapter的作用下,concept不仅仅成为类型的接口,而且弥合了同一事物的不同实现的差异。
展开阅读全文

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