concept的外快

本文通过一个复数运算的例子,探讨了概念编程(Concepts)相对于面向对象编程(OOP)的优势,特别是非侵入性和灵活性。文章展示了如何使用概念映射(concept map)将不同实现的类型统一到共同的概念之下。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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不仅仅成为类型的接口,而且弥合了同一事物的不同实现的差异。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值