Double dispatch 是为了能够通过2个对象的类型来决定调用的函数。
1: class GameObject2: {3: public:4: // 需要知道rhs的类型,才能决定如何碰撞5: virtual void Collide(GameObject& rhs) = 0;6: };7:8: class SpaceShip : public GameObject9: {10: public:11: virtual void Collide(GameObject& rhs)12: {13: // 在这里,我们知道this类型为SpaceShip14: // 通过再次调用一个重载的虚函数,即可知道rhs的类型15: rhs.Collide(*this);16: }17: };18:19: class SpaceStation : public GameObject20: {21: public:22: virtual void Collide(GameObject& rhs)23: {24: rhs.Collide(*this);25: }26: virtual void Collide(SpaceShip& rhs)27: {28: // do actual collision29: ...30: // 在这里我们知道了确切的碰撞对象,SpaceShip碰撞SpaceStation31: // 而不是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 GameObject2: {3: public:4: virtual void Collide(SpaceShip& rhs)5: {6: // 这个函数用以SpaceShip和HyperSpaceShip的碰撞7: }8: };9:10: class SpaceShip : public GameObject11: {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 GameObject3: {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接受一个SpaceStation25: // 的对象。这样的type-mismatch在单继承时不会出错,而当多继承时,就可能出现问题。/26:27: // class SpaceStation : public OtherClass, public GameObject {...}28:29: // Memory layout of SpaceStation objects30:31: // start address of ---> +--------------------+32: // SpaceStation |OtherClass Subobject|33: // ---> +--------------------+34: // |GameObject subobject|35: // +--------------------+36: // 此时编译器会传入错误的地址(GameObject的起始地址进来,但是HitSpaceStation37: // 期望的是SpaceStation类型的地址,所以调用这个对象必然会崩溃(我们在38: // GameObject子对象中请求SpaceShip才有的成员时,就会崩溃)39: virtual void HitSpaceStation(GameObject& rhs)40: {41: SpaceStation& ss = static_cast<SpaceStation&>(rhs);42: // do sth43: }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 collision5: 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: // 在这里初始化map22: }23: // 也可以创建一个类,专门用于管理这种映射关系,并使用Singletion模式24: // 可以添加,删除映射关系等25:
显然这种方法增加新的类,不需要增加成员函数。只需要定义一个全局的关于2个类型的函数即可,并进行注册,到时候获取该函数即可。
Reference:
- 《More Effective C++ 2nd》