Double dispatch 是为了能够通过2个对象的类型来决定调用的函数。
1: class GameObject
2: {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类型为SpaceShip
14: // 通过再次调用一个重载的虚函数,即可知道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 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 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接受一个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:
- 《More Effective C++ 2nd》