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不仅仅成为类型的接口,而且弥合了同一事物的不同实现的差异。
分享到:
相关推荐
施耐德电气的PLC编程软件CONCEPT v2.6是一款专为昆腾系列PLC设计的编程工具,尤其适用于CPU53414这样的控制器。这款软件是工程师们进行工业自动化项目的重要辅助工具,它提供了高效、直观的编程环境,帮助用户编写、...
施耐德小型PLC编程软件Concept是一款专为施耐德电气的小型可编程逻辑控制器(PLC)设计的集成开发环境。它提供了丰富的编程工具和功能,以支持工程师们进行高效且灵活的程序编写和系统调试。Concept软件适用于多种...
施耐德电气的PLC编程软件Concept v2.1是一款专为编程和配置施耐德自动化设备而设计的强大工具,尤其适用于昆腾系列PLC,如CPU53414等型号。这款软件集成了易用的编程环境,支持IEC 61131-3标准,使得开发者可以灵活...
dataset system concept 课件
《理解CONCEPT 2.6授权与补丁安装的关键步骤》 在计算机软件领域,授权是确保用户合法使用软件的重要环节。本文将深入探讨CONCEPT 2.6这一专业软件的授权方法及其重要性,同时也会讲解补丁安装的相关知识,帮助用户...
施耐德Concept2.6用户手册是一本为工业自动化领域设计的编程手册,专注于施耐德的PLC(可编程逻辑控制器)编程软件的使用。本手册涵盖了软件的基本概念、编程技巧、项目结构创建、PLC配置、以及不同编程语言的应用等多...
### Concept读取系统时间知识点详解 #### 一、概述 Concept是施耐德电气公司推出的一款用于编程Quantum系列PLC(可编程逻辑控制器)的软件。本文将详细介绍如何使用Concept软件来读取PLC中的系统时间,并进行必要...
《concept2.6中文手册》是专为学习和掌握Schneider Electric公司开发的Concept软件而设计的详尽指南。Concept是一款强大的编程环境,主要用于配置和编程Schneider Electric的可编程逻辑控制器(PLC)。该手册涵盖了...
Cadence_Concept_HDL&Allegro原理图与PCB设计Cadence_Concept_HDL&Allegro原理图与PCB设计Cadence_Concept_HDL&Allegro原理图与PCB设计
《施耐德PLC编程软件Concept 2.1详解》 施耐德电气是一家全球知名的自动化解决方案提供商,其产品线涵盖了各种类型的可编程逻辑控制器(PLC),在工业自动化领域广泛应用。而Concept 2.1作为施耐德的PLC编程软件,...
concept2.6 授权文件,用于昆腾老系列PLC编程软件的授权文件
CONCEPT的驱动板设计独特,易于理解和应用,尤其适合17mm双管IGBT模块。其主要由SCALE-2芯片组构成,提供了双通道驱动器,能够适应不同的工作模式。 **一、工作模式** 1. **直接模式**:当MOD输入悬空或连接到VCC时...
用于CONCEPT2.6授权,下载后直接复制到安装目录下即可!
标题中的"concept26补丁"指的是针对名为Concept 2.6软件的一个更新或修复程序。这通常是为了改进软件性能、解决已知问题或者增加新功能。在Windows 7操作系统环境下,这个补丁可能是为了确保Concept 2.6能够顺利运行...
### Concept HDL元件库到OrCAD Capture元件库的转换教程 #### 一、引言 在电子设计自动化(EDA)领域,不同的设计工具之间经常需要进行数据格式的转换以实现跨平台兼容。本文将详细介绍如何将Concept HDL工程中的...
concept2.6激活文件 可使软件正常使用
《施耐德Concept 2.5培训教程》是一份详细且全面的教学资源,旨在帮助用户掌握施耐德电气的Concept 2.5软件的使用技巧和功能。此压缩包包含了丰富的学习材料,主要针对那些想要深入理解自动化设计、编程以及控制系统...