本文来源于
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。这在静态的模板上是做不到的。