一种作用在多个对象上的虚函数。C++没有提供这样的函数,现在怎么办呢?
一种办法是扔掉C++,换种其它语言。比如,你可以改用CLOS(Common Lisp Object System)。CLOS支持绝大部分面向对象的函数调用体系中只能想象的东西:multi-method。multi-method是在任意多的参数上虚拟的函数,并且CLOS更进一步的提供了明确控制“被重载的multi-method将如何调用”的特性。
让我们假设,你必须用C++实现,所以必须找到一个方法来解决这个被称为“二重调度(double dispatch)”的问题。(这个名字来自于object-oriented programming community,在那里虚函数调用的术语是“message dispatch”,而基两个参数的虚调用是通过“double dispatch”实现的,推而广之,在多个参数上的虚函数叫“multiple dispatch”。)有几个方法可以考虑。但没有哪个是没有缺点的,这不该奇怪。C++没有直接提供“double dispatch”,所以你必须自己完成编译器在实现虚函数时所做的工作(见Item M24)。如果容易的话,我们可能就什么都自己做了,并用C语言编程了。我们没有,而且我们也不能够,所以系紧你的安全带,有一个坎途了。
l 用虚函数加RTTI
实现二重调度的最常见方法就是和虚函数体系格格不入的if...then...else链
void SpaceShip::collide(GameObject& otherObject)
{
const type_info& objectType = typeid(otherObject);
if (objectType == typeid(SpaceShip)) {
SpaceShip& ss = static_cast<SpaceShip&>(otherObject);
process a SpaceShip-SpaceShip collision;
}
…
else {
throw CollisionWithUnknownObject(otherObject);
}
}
我们的代价是几乎放弃了封装,因为每个collide函数都必须知道所以其它同胞类中的版本。尤其是,如果增加一个新的类时,我们必须更新每一个基于RTTI的if...then...else链以处理这个新的类型。即使只是忘了一处,程序都将有一个bug,而且它还不显眼。编译器也没办法帮助我们检查这种疏忽,因为它们根本不知道我们在做什么(参见Item E39)。
l 只使用虚函数
class SpaceShip; // forward declarations
class SpaceStation;
class Asteroid;
class GameObject {
public:
virtual void collide(GameObject& otherObject) = 0;
virtual void collide(SpaceShip& otherObject) = 0;
virtual void collide(SpaceStation& otherObject) = 0;
virtual void collide(Asteroid& otherobject) = 0;
...
};
class SpaceShip: public GameObject {
public:
virtual void collide(GameObject& otherObject);
virtual void collide(SpaceShip& otherObject);
virtual void collide(SpaceStation& otherObject);
virtual void collide(Asteroid& otherobject);
...
};
每个类都必须知道它的同胞类。当增加新类时,所有的代码都必须更新。不过,更新方法和前面不一样。确实,没有if...then...else需要修改,但通常是更差:每个类都需要增加一个新的虚函数。
总结一下就是:如果你需要实现二重调度,最好的办法是修改设计以取消这个需要。如果做不到的话,虚函数的方法比RTTI的方法安全,但它限制了你的程序的可控制性(取决于你是否有权修改头文件)。另一方面,RTTI的方法不需要重编译,但通常会导致代码无法维护。自己做抉择啦!
l 模拟虚函数表
对GameObjcet继承体系中的函数作一些修改:
class GameObject {
public:
virtual void collide(GameObject& otherObject) = 0;
...
};
class SpaceShip: public GameObject {
public:
virtual void collide(GameObject& otherObject);
virtual void hitSpaceShip(SpaceShip& otherObject);
virtual void hitSpaceStation(SpaceStation& otherObject);
virtual void hitAsteroid(Asteroid& otherobject);
...
};
void SpaceShip::hitSpaceShip(SpaceShip& otherObject)
{
process a SpaceShip-SpaceShip collision;
}
void SpaceShip::hitSpaceStation(SpaceStation& otherObject)
{
process a SpaceShip-SpaceStation collision;
}
void SpaceShip::hitAsteroid(Asteroid& otherObject)
{
process a SpaceShip-Asteroid collision;
}
和开始时使用的基于RTTI的方法相似,GameObjcet类只有一个处理碰撞的函数,它实现必须的二重调度的第一重。和后来的基于虚函数的方法相似,每种碰撞都由一个独立的函数处理,不过不同的是,这次,这些函数有着不同的名字,而不是都叫collide。放弃重载是有原因的,你很快就要见到的。注意,上面的设计中,有了所有其它需要的东西,除了没有实现Spaceship::collide(这是不同的碰撞函数被调用的地方)。和以前一样,实现了SpaceShip类,SpaceStation类和Asteroid类也就出来了。
在SpaceShip::collide中,我们需要一个方法来映射参数otherObject的动态类型到一个成员函数指针(指向一个适当的碰撞处理函数)。一个简单的方法是创建一个映射表,给定的类名对应恰当的成员函数指针。直接使用一个这样的映射表来实现collide是可行的,但如果增加一个中间函数lookup时,将更好理解。lookup函数接受一个GameObject参数,返回相应的成员函数指针。
这是lookup的申明:
class SpaceShip: public GameObject {
private:
typedef void (SpaceShip::*HitFunctionPtr)(GameObject&);
static HitFunctionPtr lookup(const GameObject& whatWeHit);
...
};
函数指针的语法不怎么优美,而成员函数指针就更差了,所以我们作了一个类型重定义。
既然有了lookup,collide的实现如下:
void SpaceShip::collide(GameObject& otherObject)
{
HitFunctionPtr hfp =
lookup(otherObject); // find the function to call
if (hfp) { // if a function was found
(this->*hfp)(otherObject); // call it
}
else {
throw CollisionWithUnknownObject(otherObject);
}
}
class SpaceShip: public GameObject {
private:
typedef void (SpaceShip::*HitFunctionPtr)(GameObject&);
typedef map<string, HitFunctionPtr> HitMap;
...
};
SpaceShip::HitFunctionPtr
SpaceShip::lookup(const GameObject& whatWeHit)
{
static HitMap collisionMap;
...
}
SpaceShip::HitFunctionPtr
SpaceShip::lookup(const GameObject& whatWeHit)
{
static HitMap collisionMap;
HitMap::iterator mapEntry=
collisionMap.find(typeid(whatWeHit).name());
if (mapEntry == collisionMap.end()) return 0;
return (*mapEntry).second;
}
最后一句是return (*mapEntry).second而不是习惯上的mapEntry->second以满足STL的奇怪行为。具体原因见Item M18。
l 初始化模拟虚函数表
SpaceShip::HitMap * SpaceShip::initializeCollisionMap()
{
HitMap *phm = new HitMap;
(*phm)["SpaceShip"] = &hitSpaceShip;
(*phm)["SpaceStation"] = &hitSpaceStation;
(*phm)["Asteroid"] = &hitAsteroid;
return phm;
}
l 注11:
要指出的是,不是那么可完全确定的。C++标准并没有规定type_info::name的返回值,不同的实现,其行为会有区别。(例如,对于类Spaceship,type_info::name的一个实现返回“class SpaceShip”。)更好的设计是通过它所关联的type_info对象的地址了鉴别一个类,因为每个类关联的type_info对象肯定是不同的。HitMap于是应该被申明为map<cont type_info *, HitFunctionPtr>。