OOP的黄昏
来源:互联网 发布:java aws s3 下载文件 编辑:程序博客网 时间:2024/04/28 04:36
http://www.ddj.com/cpp/218600111;jsessionid=A3DTTZ4H5WD3EQSNDLRSKH0CJUNN2JVN?pgno=2
范型这东西平时本来就很少会用到,除非写通用的库。concept对写库来说的确是好东西,但是这也不应该是在描述耦合,而是描述一种特征,STL对迭代器的特征判断是用的重载,但是对std::list的iterator遇到std::find仍然无能为力。本来范型的设计都是基于一个特征,就像类继承的设计是基于一个基类一样
concept存在的意义应该和类继承一样重要,没有concept写范型代码就好比在用C在实现OO一样,一切机遇自己的约束。有的人也会怀疑这些玩意,我觉得如果质疑模板/Concept的必要性其实就等同于站在C的角度质疑OO。那DDJ的文章也说了,现在移除掉concept是因为有一些技术问题没解决.
觉得concept就是模板(静态多态)世界中的“接口”,跟OO中动态的“接口”对照一下,至少可以加深理解。
但从应用的角度看,它们有一点微妙的差别:C++在C的基础上引入继承和多态,不仅仅使得OO代码更加优雅,而且更加安全,这一点是实实在在的。但concept,我觉得仅仅使得模板代码更加优雅,谈不上更加安全。
说得再具体一点:语言对继承和多态的支持,使得许多在运行期才能发现的错误现在可以在编译时发现,这个好处是很实在的。concept与此类似:许多在“编译模板实例代码时”才能发现的错误现在在“实例化模板时”就可以发现。——但问题是:编译器对模板的这些处理细节对程序员是透明的,所以,这一次引入“接口”所带来的“进步”也就显得更加“透明”了,不那么实在了。
本来模板的实例化动作全在编译期完成,C++也不存在运行时的模板。但是这不应该判断是否作为语言元素的一个准则,可以说concept是在完善范型机制,其次有更友好的编译错误应该算是一个附属产物,比如,回到那个std::list的问题。现在有啥办法可以让std::find支持std::list::iterator,难道只有在两个std::find的后面分别加random_accescc_iterator* = (iteartor_tag*)(0)和input_iterator* = (iteartor_tag*)(0)的参数来区分两个迭代器?SFINAE虽然是不错,但是如果一个语言啥都好各种手法或者trick来处理的话,未免也太过复杂了。本来重载是一个好东西,如果能基于concept的重载那就更好了。只是concept本身太复杂了,只是由库去实现基本上不可能了,如果引入到语言,那么STL基本会被重写,这也不是一个小的工程。如果concept有更简单的实现,估计那个委员会肯定乐意加入。
其实C++0x本来引入的concept是一个很强大的东西,不仅包含模板的类型检测,还包括,concept-map, concepte-based overload,这些都是让模板的更容易使用,更强大。
例如本来可能会引入的for-each loop.
vector<
int
> v;
for
(
int
x : v)
cout<<x<<endl;
这些都需要concept的支持。同时concept一个存在的目的也是为消除一些复杂的模板trick。
当然,所有东西在使用上都需要有正确的把握,我觉得这个就是和经验有关了。OO说,把复杂的东西简化为各种小问题,抽象会可复用的模块,至于划分到多小,那就是经验决定的,划分归纳得不够会增加耦合,划分得太细不仅耦合了而且复杂度倒还增加了。
对于concept来说,就算语言引入这样一个东西,别人可能也不会花上大量的精力去为自己的类模板写这些。其实这个问题可以退后一步来说,在平时的开发中,模板真正会用到吗?我觉得也不会,在平常的开发中,都是写一些目的明确的代码。
说concept像“接口”,感觉上是有那么一回事。一般OO的观点来说,接口其实就是一组共有的特征,你提供的东西必须要基于这些特征。而对于C++的模板来说,concept并不是用来描述一个类模板的,而是用来描述模板参数的,与算法有关。
concept Swimmable<
typename
T>
{
void
T::swim();
}
template
<
typename
T>
requires Swimmable<T>
struct
Pool
{
void
swim()
{
T t;
t.swim();
}
};
按照现在模板的特性来说,只有当调用了Pool::swim才会判断得出给定的模板参数T是否符合Pool这个模板的要求。如果有了concept,编译器立马就知道给定的模板参数T是否符合要求。其次,就算引入了concept并不会带来什么麻烦呀,它也并不会对以有的代码造成任何影响。
或许太多的思考把concept和“接口”捆绑在一起,将“接口”和concept来做比较未免有点太牵强了。首先,C++的范型编程解决的问题是用来写程序库上,C++的范型库有一个最大的优点就是,不像类框架那样,你必须要从一个“接口/基类”上派生才能在框架中工作,范型库则不需要你完成这样的前提,其次,范型库是特殊类型特殊处理,也就是说,不同的类型你扔进到一个范型库中,不需要你在乎扔进去的是什么,这点和OO的多态很像,注意一点的是,如果你用“接口”就必须回到那个前提。如果把concept单纯地看成“接口”那必然会带来束缚,现在OO的束缚就是你需要一个派生,当然这点在OO中不叫束缚,或者叫实现,但从模板的观点上来说,何尝不是束缚呢?例如,定义一个vector<类> 和vector<bool>的变量,你不用考虑vector<bool>会浪费掉不用的内存,用distance()的时候,你可以不用考虑你传入的iterator是哪种category的,但是这还是不够的,STL的典型问题就是std::find()对iterator有operator<的比较,如果是非random_access_iterator,那么是没有operator<的,当然这就暴露出模板的缺陷,如果有了concept,那么就可以判断出你给的iterator是否有operator<,如果没有则选用由operator!=做比较的std::find()。
还有一个典型问题是算法的实现是用SFINAE来判断迭代器的类型,但是我们在大多数情况下,不会在一个类中包含额外的信息,例如,你要自己实现一个兼容STL的算法iterator就必须包含一个iterator_category的typedef,也就是满足iterator的traits。如果有了concept,那这traits就完全不用了,算法可以基于concept重载。如果模板即支持(偏)特化,又支持concept,那对写通用的范型库带来极大的便利。
concept倒是有一点比OO中的接口好:就是符合某一concept的实体不必显式的声称这件事情。这一点比OO中的接口省事。
看来我前面对于boost::thread可能要依赖于boost::date_time的担心是想多了。实际情况是concept不会在依赖方面造成更高的耦合。更正一下。
正确的考量应该是:boost::thread规定它所require的时间概念,而boost::date_time中的类型只是“恰好”符合这一概念。这符合“接口应该由使用接口的client来定义,而非由实现接口的server来定义”的原则。
或许太多的思考把concept和“接口”捆绑在一起,将“接口”和concept来做比较未免有点太牵强了。
兄台所说的"接口"受了太多OO思想的影响,有点狭隘了.
接口,在一定程度上应该等同于protocol,rules.它仅仅是规范了交互的方式.从这点上来说,concept的本义也如此.
诚然,concept对于消除现在的template triks是很有意义的.但鉴于C++本身的复杂性,要把concept做得很完备,则必须引入更多的特性,到时候,template triks也许就变成了concept tricks...而现有的这些tricks又不能彻底去除.这就是concept现在的两难境地.而只做一部分concept支持又显然不是C++的风格.
既然现有的技术可以满足几乎所有的要求,就算它很恶心,但大家吐啊吐得也已经习惯得差不多了.所以,我觉得,在没有把握的情况下,也就没有必要再引入另一个同样功能而且还可能是另一个陷阱的东西了.
不过话又说回来.我觉得,如果用concept的形式,再加上type traits那种东西应该满足concept的设计要求吧.虽然这样只是换汤不换药.不过总比BOOST里那种狗屎类型约束要好看得多吧...
其实type traits也好,继承也好,他们之间的约定是在你写一个类的时候发生的,例如,你要在类里面包含一组traits,或者继承一个接口/基类。这样的范型未免太幼稚。
考虑(偏)特化,用于处理特殊的模板参数而做的匹配,并没有让使用者在用这个模板之前告诉模板需要选择哪个特化版本。拿vector做例子
class
A{};
vector<A> va;
vector<
bool
> vb;
模板在选择特化版本的时候,并不是由A和bool给出的信息去选择vector的版本,而是由模板自己去匹配该选哪个版本,这是traits方法做不到的。所以特化被看作模板一个很重要的特性。concept同样如此,当你在写一个类时,这个类将作为某个模板的参数时,concept 不会给你任何限制,而对于这个模板来说,它通过用concept的匹配就明确知道了这个模板参数具有哪样的接口,这样就可以为各种模板参数做不同的处理。
在开始,Bjarne也支持用与boost concepts相似的方法,D&E中有这样的描述,但是那样只能做到check,远远不够。其实模板上,对接口(成员函数)不同做不同的处理这样的需求早就有了,而对于这点,就算SFINAE也无能为力。
例如,考虑一下 事件回调的,例如所有的控件都有一个成员函数叫make_event,回调函数可以是一个函数对象。
template
<
typename
Function>
void
make_event(
int
event_type, Function f);
//用于注册回调的接口
--
//一般一个事件回调都会包含一个事件的信息,例如鼠标的坐标
class
functor_with_eventinfo
{
public
:
void
operator()(
const
eventinfo& ei)
{
if
(ei.mouse_x > 10 && ei.mouse_y){}
}
};
//那么在,要注册这个回调,就是
button.make_event(click, functor_with_eventinfo());
//其实大多时候,并不需要事件的信息,也就是说,我们可以写一个这样的functor
class
functor_without_eventinfo
{
public
:
void
operator()()
{
messagebox(
"hello, world"
);
}
};
button.make_event(click, functor_without_eventinfo());
这种情况的make_event倒容易解决。重载一下就搞定了。
template
<
typename
Function>
void
make_event(
int
event_type, Function f)
{
_m_make_event(event_type, f, &Function::operator());
}
template
<
typename
Function>
void
_m_make_event(
int
event_type, Function f,
void
(Function::*)());
template
<
typename
Function>
void
_m_make_event(
int
event_type, Function f,
void
(Function::*)(
const
eventinfo&));
现在问题来了,万一那个make_event的模板,由于历史原因,并不是所有的模板参数Function的类型都有operator(),有的是fire()
class
fire_event_object
{
public
:
void
fire(
const
eventinfo&)
{
messagebox(
"hello, world"
);
}
};
button.make_event(click, fire_event_object());
//注意make_event中是用&Function::operator()判断的
上面的代码不能工作了。。。其实这种问题已经存在于现实的代码中。STL的random_access_iterator就有operator+(int)的重载,而forward_iterator却没有。因此这种得约定只有靠预先在iterator的定义时包含category的玩意,问题是这样的动作是否符合范型呢?
OOP的黄昏
在“C++ Template”一书中,将多态总结为三种主要类型:runtime bound、static unbound和runtimeunbound。其中runtime bound就是我们通常所说的动多态,OOP的核心支柱(广义上OOP还包括ObjectBase(OB,仅指类型封装等OO的基本特性),但有时也会将OB和OOP分开,OOP单指以OO为基础的动多态。这里使用狭义的OOP含义);static unbound就是静多态,通过模板实现。而runtimeunbound则是一种不常见的形式。早年的SmallTalk具有这种形式,现在的ruby也引入这种机制。
在主流的(静态)语言中,我们会面临两种类型的多态需求:对于编译期可以确定类型的,使用静多态,比如实例化一个容器;对于运行期方能确定类型的,则使用动多态。而runtimeunbound也可以用于运行期类型决断。于是,便有了两种运行期多态。这两种多态的特性和他们的差异,是本文的核心。实际上,相比动多态,runtime unbound多态为我们提供了更本质的运行时多态手段,我们可以从中获得更大的收益。但是鉴于一些技术上的困难,runtimeunbound多态无法进入主流世界。不过,由于新的编程技术的出现,使得这种更好的运行时多态形式可以同动多态一比高下。
动多态
废话少说,让我们从一个老掉牙的案例开始吧:编写一个绘图程序,图形包括矩形、椭圆、三角形、多边形等等。图形从脚本(比如xml)中读出,创建后保存在一个容器中备查。通过遍历容器执行图形绘制。就这么个题目,很简单,也很熟悉,解释OOP的动多态最常用的案例。下面我们就从动多态实现开始。
首先定义一个抽象基类,也就是接口:
class IShape
{
virtual void load(xml init)=0;
virtual void draw(monitor m)=0;
...
};
然后定义各种图形类,并从这个接口上继承:
class Rectangle: public IShape
{
void load(xml init) {...}
void draw(monitor m) {...}
...
};
class Ellipse: public IShape
{
void load(xml init) {...}
void draw(monitor m) {...}
...
};
...
void DrawShapes(monitor m, vector<IShape*> const& g)
{
vector<IShape*>::const_iterator b(g.begin()), e(g.end());
for(; b!=e; ++b)
{
(*b)->draw(m);
}
}
...
现在可以使用这些图形类了:
vector<IShape*> vg;
vg.push_back(new Rectangle);
vg.push_back(new Ellipse);
...
DrawShapes(crt, vg);
通过接口IShape,我们可以把不同的图形类统一到一种类型下。但是,通过虚函数的override,由图形类实现IShape上的虚函数。这可以算老生常谈了。动多态的核心就是利用override和latebound的组合,使得一个基类可以在类型归一化的情况下,拥有继承类的语义。OOP设计模式大量运用这种技术,实现很多需要灵活扩展的系统。
Runtime Unbound
Runtime Unbound多态混合了静多态和动多态的特征,即既有类型泛化,又是运行时决断的。一个最典型的例子就是ruby的函数:class x
def fun(car)
car.aboard
end
end
这个案例非常明确地展示出了Runtime Unbound多态的特点。car参数没有类型,这里也不需要关心类型,只要求car对象有一个aboard方法即可。由于ruby是动态语言,能够运行时检测对象的特征,并动态调用对象上的方法。
在Runtime Unbound的思想指导下,我们利用一种伪造的“动态C++”,把上面的绘图例子重新编写:
class Rectangle
{
void load(xml init) {...}
void draw(monitor dev) {...}
...
};
class Ellipse
{
void load(xml init) {...}
void draw(monitor dev) {...}
...
};
...
void DrawShapes(monitor dev, vector<anything> const& g)
{
vector<IShape>::const_iterator b(g.begin()), e(g.end());
for(; b!=e; ++b)
{
(*b).draw(dev);
}
}
...
vector<anything> vg;
vg.push_back(Rectangle(...));
vg.push_back(Ellipse(...));
...
DrawShapes(crt, vg);
从这段代码中,我们可以看出RuntimeUnbound多态带来的好处。所有图形类不再需要归一化成一个类型(抽象接口)。每个类只需按照约定,实现load、draw等成员函数即可。也就是说,这些图形类解耦合了。一旦类型解耦,便赋予我们很大的自由度。最典型的情况就是,我们需要使用一个其他人开发的图形类,并且无法修改其实现。此时,如果使用动多态,就很麻烦。因为尽管这些图形类都拥有load、draw等函数,但毕竟不是继承自IShape,无法直接插入容器。必须编写一个继承自IShape的适配器,作为外来图形类的包装,转发对其的访问。表面上,我们只是减少一个接口的定义,但RuntimeUnbound多态带来的解耦有着非凡的意义。因为类耦合始终是OOP设计中的一个令人头痛的问题。在后面,我们还将看到建立在RuntimeUnbound多态基础上的更大的进步。
然而,尽管Runtime Unbound多态具有这些优点,但因为建立在动态语言之上,其自身存在的一些缺陷使得这项技术无法广泛使用,并进入主流。
Runtime Unbound多态面临的第一个问题就是类型安全。确切的讲是静态类型安全。
本质上,RuntimeUnbound多态(动态语言)并非没有类型安全。当动态语言试图访问一个未知类型对象的成员时,会通过一些特殊机制或特殊接口获得类型信息,并在其中寻找所需的对象成员。如果没有找到,便会抛出异常。但是,传统上,我们希望语言能够在编译期得到类型安全保证,而不要在运行时才发现问题。也就是说,Runtime Unbound多态只能提供运行时类型安全,而无法得到静态类型安全。
第二个问题是性能。Runtime Unbound需要在运行时搜寻类型的接口,并执行调用。执行这类寻找和调用的方法有两种:反射和动态链接。
反射机制可以向程序提供类型的信息。通过这些信息,Runtime Unbound可以了解是否存在所需的接口函数。反射通常也提供了接口函数调用的服务,允许将参数打包,并通过函数名调用。这种机制性能很差,基本上无法用于稍许密集些的操作。
动态链接则是在访问对象前在对象的成员函数表上查询并获得相应函数的地址,填充到调用方的调用表中,调用方通过调用表执行间接调用。这种机制相对快一些,但由于需要查询成员函数表,复杂度基本上都在O(n)左右,无法与动多态的O(1)调用相比。
这些问题的解决,依赖于一种新兴的技术,即concept。concept不仅很消除了类型安全的问题,更主要的是它大幅缩小了两种Runtime多态的性能差距,有望使Runtime Unbound成为主流的技术。
concept
随着C++0x逐渐浮出水面,concept作为此次标准更新的核心部分,已经在C++社群中引起关注。随着时间的推移,concept的潜在作用也在不断被发掘出来。concept主要用来描述一个类型的接口和特征。通俗地讲,concept描述了一组具备了共同接口的类型。在引入concept后,C++可以对模板参数进行约束:
concept assignable<T> {
T& operator=(T const&);
}
template<assignable T> void copy(T& a, T const& b) {
a=b;
}
这表示类型T必须有operator=的重载。如果一个类型X没有对operator=进行重载,那么当调用copy时,便会引发编译错误。这使得类型参数可以在函数使用之前便能得到检验,而无需等到对象被使用时。
另一方面,concept参与到特化中后,使得操作分派更加方便:
concept assignable<T> {
T& operator=(T const&);
}
concept copyable<T> {
T& T::copy(T const&);
}
template<assignable T> void copy(T& a, T const& b) { //#1
a=b;
}
template<copyable T> void copy(T& a, T const& b) { //#2
a.copy(b);
}
X x1,x2; //X支持operator=操作符
Y y1,y2; //Y拥有copy成员函数
copy(x1, x2); //使用#1
copy(y1, y2); //使用#2
在静多态中,concept很好地提供了类型约束。既然同样是Unbound,那么concept是否同样可以被用于RuntimeUnbound?应当说可以,但不是现有的concept。在Runtime Unbound多态中,需要运行时的concept。
依旧使用绘图案例做一个演示。假设这里使用的"C++"已经支持concept,并且也支持了运行时的concept:
class Rectangle
{
void load(xml init) {...}
void draw(monitor dev) {...}
...
};
class Ellipse
{
void load(xml init) {...}
void draw(monitor dev) {...}
...
};
...
concept Shape<T> {
void T::load(xml init);
void T::draw(monitor dev);
}
...
void DrawShapes(monitor dev, vector<Shape> const& g)
{
vector<IShape>::const_iterator b(g.begin()), e(g.end());
for(; b!=e; ++b)
{
(*b).draw(dev);
}
}
...
vector<Shape> vg;
vg.push_back(Rectangle(...));
vg.push_back(Ellipse(...));
vg.push_back(string("xxx")); //错误,不符合Shape concept
...
DrawShapes(crt, vg);
乍看起来没什么特别的,但是请注意vector<Shape>。这里使用一个concept,而不是一个具体的类型,实例化一个模板。这里的意思是说,这个容器接受的是所有符合Shape concept的对象,类型不同也没关系。当push进vg的对象不符合Shape,便会发生编译错误。
但是,最关键的东西不在这里。注意到DrawShapes函数了吗?由于vector<Shape>中的元素类型可能完全不同。语句(*b).draw(dev);的语义在静态语言中是非法的,因为我们根本无法在编译时具体确定(*b)的类型,从而链接正确的draw成员。而在这里,由于我们引入了Runtime Unbound,对于对象的访问链接发生在运行时。因此,我们便可以把不同类型的对象存放在一个容器中。
concept在这里起到了类型检验的作用,不符合相应concept的对象是无法放入这个容器的,从而在此后对对象的使用的时候,也不会发生类型失配的问题。这也就在动态的机制下确保了类型安全。动多态确保类型安全依靠静态类型。也就是所有类型都从一个抽象接口上继承,从而将类型归一化,以获得建立在静态类型系统之上的类型安全。而concept的类型安全保证来源于对类型特征的描述,是一种非侵入的接口约束,灵活性大大高于类型归一化的动多态。
如果我们引入这样一个规则:如果用类型创建实例(对象),那么所创建的对象是静态链接的,也就是编译时链接;而用concept创建一个对象,那么所创建的对象是动态链接的,也就是运行时链接。
在这条规则的作用下,下面这段简单的代码将会产生非常奇妙的效果:
class nShape
{
public:
nShape(Shape g, int n) : m_graph(g), m_n(n) {}
void setShape(Shape g) {
m_graph=g;
}
private:
Shape m_graph;
int m_n;
};
在规则的作用下,m_graph是一个动态对象,它的类型只有在运行时才能明确。但是无论什么类型,必须满足Shape concept。而m_n的类型是确定的,所以是一个静态对象。
这和传统的模板有区别吗?模板也可以用不同的类型参数定义成员数据。请看如下代码:
Rectangle r;
Ellipse e;
nShape(r, 10);
nShape.setShape(e); //对于传统模板而言,这个操作是非法的,因为e和r不是同一种类型
动态对象的特点在于,我们可以在对象创建后,用一个不同类型的动态对象代替原来的,只需要这些对象符合相应的concept。这在静态的模板上是做不到的。
下面的代码则引出了另一个重要的特性:
vector<float> vFloat; //静态对象的容器,内部存放的都是静态对象,属于同一类型float
vector<Shape> vShape; //动态对象的容器,内部存放动态对象,都符合Shape
同一个类模板,当使用类型实例化,执行static unbound多态;使用concept实例化,执行runtime unbound多态。两者的形式相同。也就是说static多态同runtime多态以相同的形式表达。由于concept的加入,两种完全不同的多态被统一在同一个模型和形式下。实际上,static和runtimeunbound多态可以看作同一个抽象体系的两个分支,分别处理不同情况的应用。而形式上的统一,则更加接近抽象体系的本质。同时,也使得两种unbound多态的差异被后台化,使用者无需额外的工作,便可以同时获得动态和静态的抽象能力。同时,两种多态所展示的逻辑上的对称性,也暗示了两者在本质上的联系。这里统一的形式,便是这种对称性的结果。
对于模板函数,则会表现出更加有趣的特性(这个函数模板有些特别,不需要template关键字和类型参数列表,这是我伪造的。但由于concept的使用,它本质上还是一个模板):
void draw(Shape g);
这个函数接受一个符合Shape的参数。如果我们用一个静态对象调用这个函数:
Rectangle r;
draw(r);
那么,就执行static unbound,实例化成一个完完整整的函数,同传统的函数模板一样。
如果用一个动态对象调用这个函数:
Shape g=Cycle();
draw(g);
g=Rectangle();
draw(g);
那么,就执行runtime unbound,生成一个等待运行时链接的函数。上面的两次调用,分别进行了两次运行时链接,以匹配不同的动态对象。
这样,我们可以通过函数调用时的参数对象,来控制使用不同的多态形式。更复杂的情况就是用一个函数的返回值调用另一个函数,这样构成的调用链依然符合上述的调用控制原则。
下面,我们将看到Runtime Unbound多态的一个精彩表演:
//假设,我们已经定义了Rectangle、Cycle、Square、Ellipse、Trangle五个类,
// 分别map到Rectangles、Cycles、Squares、Ellipses、Trangles五个concept上,
// 这些concept都refine(可以不正确地理解为继承吧)自Shape。
void draw(monitor dev, Rectangles r); //#3
void draw(monitor dev, Cycles c); //#4
void draw(monitor dev, Squares s); //#5
void draw(monitor dev, Ellipses e); //#6
void draw(monitor dev, Trangles t); //#7
//此处定义一个Shape的动态对象
Shape g=CreateShapeByUserInput(); //这个函数根据用户输入创建图形对象,所以图形对象的类型只能到运行时从能确定。
draw(crt, g);
好了,现在该调用哪个版本的draw?根据用户的输入来。换句话说,调用哪个版本的draw,取决于CreateShapeByUserInput()函数的返回结果,也就是用户输入的结果。如果CreateShapeByUserInput()返回Rectangle的动态对象,那么执行#3;如果返回的是Trangle对象,那么执行#7。这是一种动态分派的操作。在运行时concept的作用下,实现起来非常容易。对draw的调用最终会被转换成一个concept需求表,来自draw函数,每一项对应一个函数版本,并且指明了所对应的concept。动态对象上也有一个concept表,每一项存放了这个对象所符合的concept。用这两个表相互匹配,可以找到g对象的concept最匹配的那个draw版本,然后调用。
这实际上是将重载决断放到运行时进行,而concept在其中起到了匹配参数的作用。
这样的做法同利用rtti信息执行类型分派调用类似:
void draw_impl(monitor dev, Rectangle& r);
void draw_impl(monitor dev, Cycle& c);
void draw_impl(monitor dev, Square& s);
void draw_impl(monitor dev, Ellipse& e);
void draw_impl(monitor dev, Trangle& t);
void draw_impl(monitor dev, Shape& g) {
if(typeif(g)==typeid(Rectangle))
draw_impl(dev, (Rectangle&)g);
else if(typeif(g)==typeid(Cycle))
draw_impl(dev, (Cycle&)g);
...
}
但是,他们却有着天壤之别。首先,rtti分派是侵入的。如果需要增加一个图形,需要在draw函数中增加分派代码。而Runtime Unbound方案则只需要用新的concept重载draw函数即可。
其次,rtti版本有多少图形类,就需要多少if...else...,而RuntimeUnbound则是一对多的。如果有几个图形类内容不同,但有相同的接口,符合同一个concept,那么只需针对concept编写一个函数版本即可。比如,如果有一个特别的CycleEx类,使用外界正方形的左上角/右下角坐标描述,正好符合Ellipsesconcept,那么只需将CycleEx map到Ellipses上即可,无需多加任何代码。
最后,rtti需要获取类型信息,然后做线性比较,性能无法优化。但Runtime Unbound通过concept表的相互匹配,仅牵涉数值操作,有很大的优化空间。
那么这样一种运行时分派有什么好处呢?我们看到图形类上的draw函数接受一个monitor类型参数,它代表设备。如果哪一天需要向另一种设备,比如printer,输出图形,那么就需要在图形类上增加另一个版本的draw函数。如果类是别人开发的,那么就增加沟通的负担。如果类是外来的,我们无法修改,那么只能通过adapter之类的笨拙手段处理。为了让monitor之类同图形本身没有关联的东西分离,应当使用自由函数执行draw操作。但普通函数只能接受确定的类型重载,而传统的函数模板则限于编译期使用,无法进行运行时分派。所以,如果能够使用concept重载函数,并且赋予Runtime Unbound机能,那么便可以用最简单的形式针对一类类型进行处理,效能高得多。
运行时concept
语言层面的concept无法做到这些,因为它是编译期机制。为此,我们需要有一种运行时的concept,或者说二进制级别的concept。
一个concept包含了与一个或若干个类型有关的一组函数,包括成员函数和自由函数。于是,我们就可以用一个类似“虚表”的函数指针表(暂且称为ctable吧)存放concept指定的函数指针。这样的ctable依附在动态对象上,就像vtable一样。每个对象都会匹配和map到若干个concept。因此,每个动态对象会有一个concept表,其中存放着指向各ctable的指针,以及相应的concept基本信息。
当一个“用户”(函数或模板)需要在运行时链接到对象上的时候,它会提交一个concept的代码(全局唯一)。系统用这个代码在动态对象的concept表上检索,获得指向所需concept的指针,并且填写到“用户”给出的一个“插入点”(一个指针)中。随后“用户”便可以直接通过这个“插入点”间接调用所需的函数,成员或自由函数。
在这里,concept的巧妙之处在于,将一族函数集合在一起,作为一个整体(即接口)。那么,在执行运行时匹配的时候,不再是一个函数一个函数地查询,可以一次性地获知这些函数是否存在。这就很容易地规避了类型安全保证操作的损耗。如果使用hash查询,那么可以在O(1)实现concept匹配。另外,一个concept的hash值可以在编译时计算好,运行时链接只需执行hash表检索,连hash值计算也可以省去。
一个动态对象可以直接用指向concept ctable的指针表示。在不同concept之间转换,相当于改变指针的指向,这种操作非常类似OOP中的dynamic_cast。
对于如下的动态对象定义:
Shape g=Cycle();
会创建一个Cycle对象,在对象上构建起一个concept表,表中对应Cycle所有符合的concept。并且建立一组ctable,每个ctable对应一个concept。每个concept表项指向相应的ctable。而符号g则实际上是指向所建立对象的Shapesctable的指针。
对于函数:
void draw(Shape g);
draw(g);
调用g时,由于draw的参数是Shapeconcept,而g正是draw所需的concept,所以无需在对象g的concept表上匹配,可以直接使用这个ctable指针。这就是说,只要所用动态对象(g)的concept同使用方(draw函数)能够匹配,便可以直接使用指向ctable的指针链接(编译时链接),无需在运行时重新匹配。只有发生concept转换时,才需要在concept表中搜索,获得所需的ctable指针:
Swappable s=g; //Swappable是另一个concept
这种情况同dynamic_cast极其相似。也可以模仿着采用concept_cast之类的操作符,使得concept转换显式化,消除隐式转换的问题(强concept化)。
Runtime Unbound和Runtime Bound
对于runtime unbound同runtime bound之间的差异前面已经有所展示。在其他方面,两者还存在更多的差别。首先,就像绘图案例中展示的那样,runtime unbound是非侵入的。runtime unbound不要求类型继承自同一类型,只需将类型同concept关联起来便可。
其次,concept不是一种局限于OO的技术,不仅涉及成员函数,还包括了自由函数,范围更广,更加灵活。
最后,实现上,Runtime Unbound和RuntimeBound之间有惊人的相似之处。两者都采用一个函数指针表作为操作分派;都采用一个指向函数表的指针作为入口;一个动态对象上的concept之间的转换,也同动多态对象一样,在不同的函数表间切换。他们唯一的不同,是实现接口的机制。
动多态用类型兼任接口,通过继承和虚函数实现接口的功能。用类型作为类型的接口,使得这两个本来独立的概念交织在一起。增加整个类型体系的复杂度和耦合度。 concept则利用独立的系统描述、表达和管理接口。类型则回归到表达业务对象的功能上来。
动多态在使用类型表达接口的时候,便很容易地引入一个麻烦的问题,表达功能的类型和表达接口的类型混合在一起,使用时必须通过一些方法区分出哪些是接口,哪些是功能类型。这增加了对象模型的复杂性。而concept则独立于类型体系之外,所有对接口的操作都是单一的,检索和匹配来得更加方便快捷。
作为继承体系的基础部分,动多态的抽象接口必须在继承结构的最顶端。那么这些抽象类型必须先于其他类型出现。这对系统的早期设计产生很大的压力,往往一个基础抽象接口设计有误,便会造成整个体系的变更。
而concept是独立于类型的,那么任何时候都可以将一个类型同接口绑定。接口甚至可以在类型体系基本建立之后才确定。这种灵活性对复杂软件的开发至关重要,去掉了长期以来套在人们头上的枷锁。
前面已经提到,在不需要concept转换的情况下,无需执行运行时的concept匹配,所有的调用具有同动多态一样的效率(都是间接调用)。在执行concept转换时,无需象动多态那样在复杂的继承体系上检索,只需执行concept表的hash匹配,效率反而更高,而且更简单。考虑到这些情况,我们可以认为concept化的Runtime Unbound多态完全能够替代传统的动多态。也就是说,我们不再需要动多态了。
想象一下,如果一门语言能够拥有运行时concept,那么它完全可以只保留Static Unbound和RuntimeUnbound多态,而放弃RuntimeBound多态。一旦放弃动多态(没有了虚函数和虚表),那么对象模型便可以大大简化。所有对象只需要线性分布,基类和成员依次堆叠在一起,也没有vtable的干扰,对象结构可以做到最简单。同时,继承也回归了代码重用的传统用途。而且,对象独立于接口存储,在能够在编译时静态链接的时候,可以作为静态对象使用。而在需要动态对象的地方,又可以很容易地转换成动态对象,只需要为其附上concept表和ctable。一切都简化了。对象模型也更加容易统一。
这对于很多底层开发的程序员对于c++复杂而又混乱的对象模型难以接受。如果能够废除虚函数,简化对象模型,那么对于这些底层开发而言,将会带来直接的好处。只要确保不使用concpt定义对象、实例化模板,便可以使整个软件执行StaticUnbound。这相当于去掉OOP的C++。否则,就启用Runtime Unbound,实现运行时多态。
总结
Static Unbound和RuntimeUnbound作为一对亲密无间的多态技术,体现了最完善的抽象形式。两者各踞一方,相互补充,相互支援。而且两者具有统一的表现形式,大大方便了使用,对于软件工程具有非凡的意义。另一方面,RuntimeBound多态作为OO时代的产物,体现了静态类型语言在运行时多态方面的最大努力。但是,随着运行时concept的引入,RuntimeUnbound多态自身存在的静态类型安全问题和性能问题,都能够得到很好的解决。至此,Runtime Unbound便具备了替代RuntimeBound的实力。相信在不久的将来,Runtime Bound将会逐渐步入它的黄昏。参考
- http://groups.google.com/group/pongba/web/Runtime+Polymorphic+Generic +Programming.pdf。大牛人Jaakko Järvi等写的关于Runtime concept的文章,讲解了runtime concept的概念的实现方法,并在ConceptC++上以库的形式实现。其中使用传统的动多态实现runtime concept,这表明动多态的实现机制同runtime concept是一致的。当然库的实现很复杂,这是“螺蛳壳里做道场”,无奈之举。Runtime concept还是应当在语言中first-class地实现。
- http://www.lubomir.org/academic/MinimizingCodeBloat.pdf。也是Jaakko Järvi写的,运行时分派的文章。
- http://opensource.adobe.com/wiki/index.php/Runtime_Concepts。
- Inside C++ Object Model。
附录 Runtime Concept的具体实现
我们有一个concept:concept Shape<T>
{
void T::load(xml);
void T::draw(device);
void move(T&);
}
另外,还有一个代表圆的concept:
concept Cycles<T> :
CopyConstructable<T>,
Assignable<T>,
Swappable<T>,
Shape<T>
{
T::T(double, double, double);
double T::getX();
double T::getY();
double T::getR();
void T::setX(double);
void T::setY(double);
void T::setR(double);
}
现在有类型Cycle:
class Cycle
{
public:
Cycle(double x, double y, double r);
Cycle(Cycle const& c);
Cycle& operator=(Cycle const& c);
void swap(Cycle const& c);
void load(xml init);
void draw(device dev);
double getX();
double getY();
double getR();
void setX(double x);
void setY(double y);
void setR(double r);
private:
...
};
当定义一个动态对象:
Shape g=Cycle();
便会形成如下图的结构:
如果遇到语句:
Swappable h=concept_cast<Swappable>(g);
那么,将会执行一个搜索,用conceptSwappable的id(比如hash码)在concept表中检索是否存在Swappable项。如果存在,就将对应项的指针赋给h。这种操作同dynamic_cast操作非常相似,只是相比在复杂的对象结构中查询更加简单迅速。
concept表置于对象的头部或尾部,这是为了便于对象检索concept接口。每个类型的ctable只需一份。
对象本体可以很容易地同concept表分离,在完全静态的情况下,concept表是不需要的。如果需要runtime多态,加上concept表即可。
Feedback
不过这个语言级的新特性,还要等两年啊 回复 更多评论
后来高手又使用template和template的特化,整出一个编译时多态。
上面的这2个倒是还好理解,现在有真个。。。多态
要用concept来实现,但是我看concept的作用其实就是一个接口,一个抽象。(感觉跟以前没有什么大的差别哦,也许是我没有仔细看哦!希望高手过来指点哦) 回复 更多评论
回复 更多评论
不过我希望lambda特性能被加入
boost 回复 更多评论
受益匪浅!
自己模拟了一下
http://blog.csdn.net/cchhope/archive/2008/04/18/2304969.aspx
- OOP的黄昏
- OOP的黄昏
- OOP的黄昏
- OOP的黄昏
- OOP的黄昏
- 温馨的黄昏
- 诸神的黄昏
- 黄昏下的变色龙
- 医院的黄昏
- 老牌手机的黄昏
- 老牌手机的黄昏
- 黄昏
- 黄昏
- 城市里黄昏的公交车
- 在今天的黄昏里
- 黄昏的长嘴小鸟
- 泪流不止~~早来的黄昏
- 写于杭州的一个黄昏
- 关于C++模板实例化后的连接问题
- 文件读写和注册表读写
- 在线学习编程
- 调试发行版程序 (一)
- C#访问器
- OOP的黄昏
- usp_blocker
- C# Winform 程序打包安装小结
- ubuntu下添加python模块
- 调试发行版程序 (二)
- 数据结构:线段树
- java jps 失效问题
- 无需第三方软件即可查询好友IP地址 (1)
- 调试发行版程序 (三)