当前位置: 代码迷 >> 综合 >> Double Dispatch
  详细解决方案

Double Dispatch

热度:69   发布时间:2024-01-17 20:35:20.0

Double dispatch 是为了能够通过2个对象的类型来决定调用的函数。

  1: class GameObject
  2: {
  3: public:
  4: 	// 需要知道rhs的类型,才能决定如何碰撞
  5: 	virtual void Collide(GameObject& rhs) = 0; 
  6: };
  7: 
  8: class SpaceShip : public GameObject
  9: {
 10: public:
 11: 	virtual void Collide(GameObject& rhs)
 12:     {
   
 13: 	    // 在这里,我们知道this类型为SpaceShip
 14: 	    // 通过再次调用一个重载的虚函数,即可知道rhs的类型
 15: 	    rhs.Collide(*this); 
 16:     }
 17: };
 18: 
 19: class SpaceStation : public GameObject
 20: {
 21: public:
 22: 	virtual void Collide(GameObject& rhs)
 23: 	{
 24: 	    rhs.Collide(*this);
 25: 	}
 26: 	virtual void Collide(SpaceShip& rhs)
 27: 	{
 28: 	    // do actual collision
 29: 	    ...
 30: 	    // 在这里我们知道了确切的碰撞对象,SpaceShip碰撞SpaceStation
 31: 	    // 而不是SpaceStation碰撞SpaceShip,这有点违反直觉
 32: 	}
 33: };
 34: 
 35: int main()
 36: {
 37: 	GameObject* ship = new SpaceShip;
 38: 	GameObject* station = new SpaceStation;
 39: 	
 40: 	// 这样就可以根据2种类型在运行时决定碰撞效果了
 41: 	// 编译器先调用SpaceShip::Collide(GameObject&)
 42: 	// 之后再调用SpaceStation::Collide(SpaceShip&)
 43: 	ship->Collide(station);
 44: }

但是double dispatch有个缺点就是,每当我需要在继承体系中加入新的类时,必须更改所有的类,加入对该类的重载:

  1: class HyperSpaceShip : public GameObject
  2: {
  3: public:
  4:     virtual void Collide(SpaceShip& rhs)
  5:     {
  6:         // 这个函数用以SpaceShip和HyperSpaceShip的碰撞
  7:     }
  8: };
  9: 
 10: class SpaceShip : public GameObject
 11: {
 12: public:
 13:     // 新加的
 14:     virtual void Collide(HyperSpaceShip& rhs);
 15: };
 16: 

自定义虚函数表

通过使用RTTI来决定调用哪个函数:

1. 还是使用虚函数,通过一个虚函数调用决定左操作数类型后,再根据参数的RTTI类型,来调用合适的成员函数。(由于加入新的类,还是需要增加成员函数,所以和double dispatch有类似的问题)。

2. 通过2种类型的RTTI来调用非成员函数(可以解决加入新的类后,需要修改现有类的定义的问题)。

第一种方法:

  1: // GameObject同上
  2: class SpaceShip : public GameObject
  3: {
  4: public:
  5:     typedef void (SpaceShip::* CollideFunc)(GameObject&);
  6:     // 没有必要存储std::string,因为typeid返回的type_info是全局对象,
  7:     // 可以直接存放std::type_info*
  8:     typedef std::map<std::string, CollideFunc> CollideMap;
  9:     virtual void Collide(GameObject& rhs)
 10:     {
 11:         CollideFunc f = Lookup(typeid(rhs).name());
 12:         if (f) {
 13:             (this->*f)(rhs);
 14:         } else {
 15:             assert(false);
 16:         }
 17:     }
 18: 
 19:     // 这里不像double dispatch使用具体类型,因为
 20:     // 1. 如果是不同类型,则无法存入一个map中
 21:     // 2. 如果是void HitSpaceStation(SpaceStation&),可以通过强制转换放入
 22:     // CollideMap中。当我们通过Collide(GameObject&)来调用时,假设找到了调用函数
 23:     // 编译器调用时的函数签名为 void (SpaceShip::* CollideFunc)(GameObject&),
 24:     // 所以传入的是GameObject的起始地址,而HitSpaceStation接受一个SpaceStation
 25:     // 的对象。这样的type-mismatch在单继承时不会出错,而当多继承时,就可能出现问题。/
 26: 
 27:     // class SpaceStation : public OtherClass, public GameObject {...}
 28: 
 29:     // Memory layout of SpaceStation objects
 30: 
 31:     // start address of ---> +--------------------+
 32:     // SpaceStation          |OtherClass Subobject|
 33:     //                  ---> +--------------------+
 34:     //                       |GameObject subobject|
 35:     //                       +--------------------+
 36:     // 此时编译器会传入错误的地址(GameObject的起始地址进来,但是HitSpaceStation
 37:     // 期望的是SpaceStation类型的地址,所以调用这个对象必然会崩溃(我们在
 38:     // GameObject子对象中请求SpaceShip才有的成员时,就会崩溃)
 39:     virtual void HitSpaceStation(GameObject& rhs)
 40:     {
 41:         SpaceStation& ss = static_cast<SpaceStation&>(rhs);
 42:         // do sth
 43:     }
 44: 
 45:     static CollideFunc Lookup(const std::string& name)
 46:     {
 47:         // 这里不直接初始化,因为即使是静态对象,虽然构建一次,但是对于表的初始化会因为
 48:         // 函数的多次调用而被初始化多次,没有意义,通过在另一个函数中初始化后再返回引用
 49:         // 即可解决
 50:         // MC++这里是使用static auto_ptr<…>来保存返回值
 51:         static CollideMap& cm = Init(); 
 52:         return cm[name];
 53:     }
 54: 
 55:     static CollideMap& Init()
 56:     {
 57:         // more effective c++这里是返回一个指针
 58:         static CollideMap cm;
 59:         // 这里不应该使用字符串作为key,因为标准未对type_info::name的返回值进行规定,
 60:         // type_info::name返回的值在不同编译器之间不可移植
 61:         // 而通过使用type_info对象的指针就可以解决
 62:         cm[“SpaceStation”] = &HitSpaceStation;
 63:         cm[“HyperSpaceShip”] = &HitHyperSpaceShip;
 64:         return cm;
 65:     };
 66: };
 67: 

第二种方法:

  1: // 全局函数
  2: void SpaceShipHitSpaceStation(GameObject& lhs, GameObject& rhs)
  3: {
  4:     // do collision
  5:     SpaceShip& ship = static_cast<SpaceShip&>(lhs);
  6:     SpaceStation& station = static_cast<SpaceStation&>(rhs);
  7: }
  8: 
  9: typedef void (* CollideFunc)(GameObject&, GameObject&);
 10: // 保存碰撞的2个类名
 11: typedef std::pair<std::string, std::string> CollideObjectsType;
 12: typedef std::map<CollideObjectsType, CollideFunc> CollideMap;
 13: CollideFunc Lookup(const std::string& name1, const std::string& name2)
 14: {
 15:     // 采用2个类名的pair在map中找
 16: }
 17: 
 18: // 初始化
 19: int main()
 20: {
 21:     // 在这里初始化map
 22: }
 23: // 也可以创建一个类,专门用于管理这种映射关系,并使用Singletion模式
 24: // 可以添加,删除映射关系等
 25: 

显然这种方法增加新的类,不需要增加成员函数。只需要定义一个全局的关于2个类型的函数即可,并进行注册,到时候获取该函数即可。

Reference:

  1. 《More Effective C++ 2nd》
  相关解决方案