OOP的黄昏

longshanks 2007-12-06 05:16:10
本文来源于TopLanguage Group 上的一次讨论(这里这里这里 )。pongba提出:C++的抽象机制并不完善,原因是为了性能而做的折中,未来随着计算能力的提高到一定程度,人们就能够忽略更好的抽象所带来的负面效应。就此诸老大各自提出高见,受益良多啊。经过讨论,我基本上理解了pongba的想法。但我觉得等待计算机的性能提高太消极了。我相信随着编程技术的发展,这种最优抽象造成的性能损失将会越来越小。这种途径将会更快地让人们接受最优抽象形式。

在“C++ Template”一书中,将多态总结为三种主要类型:runtime bound、static unbound和runtime unbound。其中runtime bound就是我们通常所说的动多态,OOP的核心支柱(广义上OOP还包括Object Base(OB,仅指类型封装等OO的基本特性),但有时也会将OB和OOP分开,OOP单指以OO为基础的动多态。这里使用狭义的OOP含义); static unbound就是静多态,通过模板实现。而runtime unbound则是一种不常见的形式。早年的SmallTalk具有这种形式,现在的ruby也引入这种机制。
在主流的(静态)语言中,我们会面临两种类型的多态需求:对于编译期可以确定类型的,使用静多态,比如实例化一个容器;对于运行期方能确定类型的,则使用 动多态。而runtime unbound也可以用于运行期类型决断。于是,便有了两种运行期多态。这两种多态的特性和他们的差异,是本文的核心。实际上,相比动多态, runtime unbound多态为我们提供了更本质的运行时多态手段,我们可以从中获得更大的收益。但是鉴于一些技术上的困难,runtime unbound多态无法进入主流世界。不过,由于新的编程技术的出现,使得这种更好的运行时多态形式可以同动多态一比高下。
动多态
废话少说,让我们从一个老掉牙的案例开始吧:编写一个绘图程序,图形包括矩形、椭圆、三角形、多边形等等。图形从脚本(比如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和late bound的组合,使得一个基类可以在类型归一化的情况下,拥有继承类的语义。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);
图形类不再从抽象接口IShape继承,而用关键字anything实例化vector<>模板。这个虚构的anything关键字所起的作 用就是使得vector能够接受不同类型的对象。当DrawShapes()函数接收到存放图形对象的容器后,遍历每一个对象,并且调用对象上的draw ()函数,而不管其类型。
从这段代码中,我们可以看出Runtime Unbound多态带来的好处。所有图形类不再需要归一化成一个类型(抽象接口)。每个类只需按照约定,实现load、draw等成员函数即可。也就是 说,这些图形类解耦合了。一旦类型解耦,便赋予我们很大的自由度。最典型的情况就是,我们需要使用一个其他人开发的图形类,并且无法修改其实现。此时,如 果使用动多态,就很麻烦。因为尽管这些图形类都拥有load、draw等函数,但毕竟不是继承自IShape,无法直接插入容器。必须编写一个继承自 IShape的适配器,作为外来图形类的包装,转发对其的访问。表面上,我们只是减少一个接口的定义,但Runtime Unbound多态带来的解耦有着非凡的意义。因为类耦合始终是OOP设计中的一个令人头痛的问题。在后面,我们还将看到建立在Runtime Unbound多态基础上的更大的进步。
然而,尽管Runtime Unbound多态具有这些优点,但因为建立在动态语言之上,其自身存在的一些缺陷使得这项技术无法广泛使用,并进入主流。
Runtime Unbound多态面临的第一个问题就是类型安全。确切的讲是静态类型安全。
本质上,Runtime Unbound多态(动态语言)并非没有类型安全。当动态语言试图访问一个未知类型对象的成员时,会通过一些特殊机制或特殊接口获得类型信息,并在其中寻 找所需的对象成员。如果没有找到,便会抛出异常。但是,传统上,我们希望语言能够在编译期得到类型安全保证,而不要在运行时才发现问题。也就是说, 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是否同样可以被用于Runtime Unbound?应当说可以,但不是现有的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。这在静态的模板上是做不到的。
...全文
1480 100 打赏 收藏 转发到动态 举报
写回复
用AI写文章
100 条回复
切换为时间正序
请发表友善的回复…
发表回复
lihuiba 2008-02-19
  • 打赏
  • 举报
回复
Mephisto_76总结的非常精辟!

快要出来的C++0x能做到这些么?
longshanks 2007-12-29
  • 打赏
  • 举报
回复
也就是说,g的concept由g的编写者在编译时决定,而在运行时由concept的需求者来匹配这些预定义的concept?
=========================
没错,在这里(runtime concept)可以看作是一种接口,相比抽象基类更灵活、更强大的接口。
qingcairousi 2007-12-28
  • 打赏
  • 举报
回复
也就是说,g的concept由g的编写者在编译时决定,而在运行时由concept的需求者来匹配这些预定义的concept?
longshanks 2007-12-28
  • 打赏
  • 举报
回复
我指的是对象g的concept表。既然concept是一组函数执行匹配,那对象g的成员函数的组合可以产生的concept就太多了,怎么解决这种问题?
=====================
哦,这得从concept本身说起:
concept可以看作一种对类型的描述,描述了类型的接口特征。主要包含了类型的成员函数和作用于类型的自由函数(包括操作符)。
一个concept是成员函数的组合,但不一定是全部。那么如何确定这样一种组合?通过concept定义和concept_map实现:
concept concept1<T> {
void T::fun1(); //成员
void fun2(T& t); //自由函数
}
concept_map<MyType>{}
这样,我们定义了一个concept并且将concept同一个类型绑定。concept还可以自动绑定,通过使用auto关键字。
因此,concept确定的接口函数组合是由程序员控制的。对于那些没有定义concept的组合,将不做考虑,编译器会认为这些组合是不存在的。
更多的内容可以参考concept的提案这篇文章
Lineric 2007-12-28
  • 打赏
  • 举报
回复
up
dazhuaye 2007-12-27
  • 打赏
  • 举报
回复
复杂有挑战~!`
hfgayy 2007-12-27
  • 打赏
  • 举报
回复
tag!
qingcairousi 2007-12-27
  • 打赏
  • 举报
回复
对draw的调用最终会被转换成一个concept需求表,来自draw函数,每一项对应一个函数版本,并且指明了所对应的 concept。动态对象上也有一个concept表,每一项存放了这个对象所符合的concept。用这两个表相互匹配,可以找到g对象的 concept最匹配的那个draw版本,然后调用。
......
对于如下的动态对象定义:
Shape g=Cycle();
会创建一个Cycle对象,在对象上构建起一个concept表,表中对应Cycle所有符合的concept。并且建立一组ctable,每个 ctable对应一个concept。每个concept表项指向相应的ctable。而符号g则实际上是指向所建立对象的Shapes ctable的指针。


============================================================
我觉得这有点问题。concept的组合方式太灵活了,几乎可以说是无穷的。如果Cycle是一个有几十个接口的对象,仅仅是考虑简单的排列组合,它的concept表都将庞大得不可想象。可能最后的效率还不如简单的反射
dazhuaye 2007-12-27
  • 打赏
  • 举报
回复
逛逛~!`
sanban 2007-12-27
  • 打赏
  • 举报
回复
也来看看
sanban 2007-12-27
  • 打赏
  • 举报
回复
也来看看
qingcairousi 2007-12-27
  • 打赏
  • 举报
回复
另外,concept代表一组函数执行匹配,concept匹配了,其中的函数也匹配了。

======================
CSDN的blog好像坏了,我这边打不开。
我指的是对象g的concept表。既然concept是一组函数执行匹配,那对象g的成员函数的组合可以产生的concept就太多了,怎么解决这种问题?
longshanks 2007-12-27
  • 打赏
  • 举报
回复
哦,太抱歉,因为有图片,帖子里没有放附录。
在我的blog里有这个附录,看我blog里的那个版本吧。
http://blog.csdn.net/longshanks
longshanks 2007-12-27
  • 打赏
  • 举报
回复
我不太清楚你所说的concept组合是指什么。是指draw的concept需求表,还是对象g的concept表。
如果是前者,我们不需要所有的concept的组合,只有那些在draw上声明和定义了的组合才会用于构造concept需求表:
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
除了这些组合以外的concept组合,都将会引发编译错误或运行时错误(取决于能否在编译时决断)。或者说,这是由程序员控制的。而不是穷尽所有组合。
如果是后者,请参见附录。concept中的ctable在实现上等价于动多态的vtable,concept/ctable和对象的数量关系与interface/vtable和对象的数量关系之间是同等的。也就是说,从ctable的数量上来说,不会比vtable更糟。既然现在人们能够容忍动多态和vtable,那么也应当可以接受runtime concept。
更重要的是,ctable对于一个类型的所有对象都只需要一份,而不需要象vtable那样由对象持有和维护(有多少对象,就有多少套vtable)。
说到性能,在runtime concept作用下,绝大多数情况都仅仅是间接调用,就像动多态的interface那样。只有在concept转换时才涉及到concept的检索。利用hash,可以在O(1)下实现检索。反射通常也可以在O(1)下完成,但是只要涉及到调用,就必须执行检索。
另外,concept代表一组函数执行匹配,concept匹配了,其中的函数也匹配了。而反射则需要挨个匹配函数,他们之间的差异是以所针对该对象调用的函数个数为倍数的。
//concept
void fun(concept1 a) {
a.f1(); //不执行匹配,只执行间接调用
a.f2(); //不执行匹配,只执行间接调用
f3(a); //不执行匹配,只执行间接调用
}
concept1 x1=...;
concept2 x2=...; //x2指向的对象同时具备concept1
fun(x1); //无需匹配,concept相符
fun(concept_cast<concept1>(x2)); //为确保“强concept”,需要concept转换,执行一次concept匹配
//反射
void fun(a) { //需要分别为每个函数调用匹配
a.f1(); //匹配f1()
a.f2(); //匹配f2()
f3(a); //匹配f3()
}
czpsailersunspot 2007-12-25
  • 打赏
  • 举报
回复
留名学习
lixkyx 2007-12-25
  • 打赏
  • 举报
回复
楼主高瞻远瞩,佩服。
Mephisto_76 2007-12-25
  • 打赏
  • 举报
回复
concept和interface相比的一个优点是concept可以完成interface同样的约束条件,但是对客户的非必要性强制更小。
一个使用interface的方法,必须和明确实现该interface的类型配合使用,而concept则不需要这样的约束,他们可以银式的被推导或者通过concept map来由使用者装配。相比于interface约束,这种方式,对于库的实现制约更小,而对业务需求变化频繁的领域,更加有益。

一个语法糖、一个很简单的中间层、一点貌似微不足道的变化,单一的来看,都没有什么。但是正是这一点点积累,造成了最终的质变。
longshanks 2007-12-25
  • 打赏
  • 举报
回复
to Mephisto_76:
精辟!:)
batistawjp 2007-12-12
  • 打赏
  • 举报
回复
jf~
zyw2007 2007-12-12
  • 打赏
  • 举报
回复
顶,明天在看
加载更多回复(80)

64,637

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧