concept的外快
Concept,一个逝去的梦,未来的希望,抽象之毒的解药,...
最后,在用之前,我们必须将类型和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作为返回类型。这直接导致了两个操作同这两个 具体类型的依赖。我们不希望一个通用性的操作同一个具体类型相关,因为不利于提高抽象度。解决的方法有这样几种:
最简单的,使用在算法中使用类型别名,而非具体类型:
在导入+操作前(include),定义ComplexPlusRet即可。这种方案尽管简单,但只是提供了一个间接,并未彻底消除两者的依赖关系。
稍微复杂些的,就是将这个类型别名的定义放入concept:
concept RectComplex
{
typedef BenComplex RetType;
...
};
这种方式更方便些,但同样也没有彻底消除依赖。因为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的对象作为+的参数时,不能直接使用它的构造函数:
AsslyComplex x, y, z;
z=x+y;
这样的代码是错误的。此时,lhd的类型,即typeof(lhd),是AsslyComplex。使用它的构造函数,会将原来的实部/虚部作为模/幅角创建AsslyComplex对象,产生错误的结果。
现在,我们在两个concept中增加创建函数,也可以认为是concept的“构造函数”:
RectComplex(float real, float img) ;
PolarComplex(float mag, float angle);
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) {
代码中的粗体部分就是答案。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不仅仅成为类型的接口,而且弥合了同一事物的不同实现的差异。