编程交流与学习--More Effective C++的学习-Item M31:让函数根据一个以上的对象来决定怎么虚拟

本文探讨了C++中实现多重调度的各种方法,包括利用虚函数和RTTI进行碰撞检测的具体实现,以及通过模拟虚函数表达成更为灵活的调度机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一种作用在多个对象上的虚函数。C++没有提供这样的函数,现在怎么办呢?

一种办法是扔掉C++,换种其它语言。比如,你可以改用CLOSCommon Lisp Object System)。CLOS支持绝大部分面向对象的函数调用体系中只能想象的东西:multi-methodmulti-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函数都必须知道所以其它同胞类中的版本。尤其是,如果增加一个新的类时,我们必须更新每一个基于RTTIif...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);

  ...

};

函数指针的语法不怎么优美,而成员函数指针就更差了,所以我们作了一个类型重定义。

既然有了lookupcollide的实现如下:

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的返回值,不同的实现,其行为会有区别。(例如,对于类Spaceshiptype_info::name的一个实现返回“class SpaceShip”。)更好的设计是通过它所关联的type_info对象的地址了鉴别一个类,因为每个类关联的type_info对象肯定是不同的。HitMap于是应该被申明为map<cont type_info *, HitFunctionPtr>

 

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值