我的C++实践(18):多态的双重分派实现

    一般的多态是单重分派,即一个基类指针(或引用)直接到绑定到某一个子类对象上去,以获得多态行为。在前面“多态化的构造函数和非成员函数”介绍中,非成员函数函数operator<<实现了单重分派,它只有一个多态型的参数,即基类引用NLComponent&,通过在继承体系中定义一个统一的虚函数接口print来完成实际的功能,然后让operator<<的NLComponent&引用直接调用它即可,就可以自动地分派到某一个子类的print上去。
    但很多时候我们需要双重分派或多重分派。比如有一个外太空天体碰撞的视频游戏软件,涉及到宇宙飞船SapceShip、太空站SpaceStation、小行星Asteroid,它们都继承自GameObject。当天体碰撞时,需要调用processCollision(GameObject& obj1,GameObject& obj2)来进行碰撞处理,不同天体之间的碰撞产生不同的效果。这里有两个基类引用型的参数,它们的动态类型不同时需要做不同的碰撞处理,这就是双重分派。一种实现方案类似于前面的NLComponent,在各个天体类中定义统一的虚函数接口collide(GameObject&,GameObject&)来完成实际的碰撞处理,在processCollision中调用它即可。这样,在collide中我们要用一大堆的if/else来判断参数的动态类型(用typeid),根据不同的动态类型调用不同的碰撞处理函数,这种方法显然非常糟糕,它使得一个天体类需要知道它所有的兄弟类,特别地,如果增加一个新类(比如Satellite),那所有的类都需要修改collide,以增加对这个新类的判断,然后重新编译全部的代码。
    如果分析虚函数的实现机理,我们知道虚函数在编译器中通过虚函数表来实现,它是一个函数指针数组,数组的每个元素是一个函数指针,指向了实际要调用的虚函数,每个函数指针有一个唯一的下标索引,通过下标索引可以直接定位到该函数指针入口。这就启示我们,可以通过模拟虚函数表来实现双重分派。
    1、模拟虚函数表。我们把各个碰撞函数实现为非成员函数,参数的不同动态类型对应不同的碰撞函数。它们接受的参数都是两个GameObject&引用,这样所有的碰撞函数都具有相同的类型。定义一个map用来存放这种类型的函数指针,用函数参数的动态类型名称作为唯一的索引,由于有两个参数,因此把它们捆绑成一个pair对象来作为唯一的索引。这样,在processCollision中,直接根据两个参数的动态类型名称查找函数表,找到接受此参数的函数指针,然后调用这个碰撞函数进行处理即可。
    下面是天体类的继承体系:

//GameObject.hpp:太空游戏的框架 #ifndef GAME_OBJECT_HPP #define GAME_OBJECT_HPP class GameObject{ //表示天体的抽象基类 public: //... virtual ~GameObject()=0; }; GameObject::~GameObject(){ //纯虚的析构函数必须有定义 } class SpaceShip : public GameObject{ //飞船类 public: //... }; class SpaceStation : public GameObject{ //空间站类 public: //... }; class Asteroid : public GameObject{ //小行星类 public: //... }; #endif

    下面是碰撞处理的实现:

//collision.hpp:碰撞处理 #ifndef COLLISION_HPP #define COLLISION_HPP #include #include //用到了pair及auto_ptr #include #include "GameObject.hpp" namespace{ //主要的碰撞处理函数 void shipStation(GameObject& spaceShip,GameObject& spaceStation){ //处理SpaceShip-SpaceStation碰撞:比如让双方遭受与碰撞速度成正比的损坏 } void shipAsteroid(GameObject& spaceShip,GameObject& asteroid){ //处理SpaceShip-Asteroid碰撞 } void stationAsteroid(GameObject& spaceStation,GameObject& asteroid){ //处理SpaceStation-Asteroid碰撞 } void shipShip(GameObject& spaceShip1,GameObject& spaceShip2){ //处理SpaceShip-SpaceShip碰撞 } void stationStation(GameObject& spaceStation1,GameObject& spaceStation2){ //处理SpaceStation-SpaceStation碰撞 } void asteroidAsteroid(GameObject& asteroid1,GameObject& asteroid2){ //处理Asteroid-Asteroid碰撞 } //对称的版本 void stationShip(GameObject& spaceStation,GameObject& spaceShip){ shipStation(spaceShip,spaceStation); } void asteroidShip(GameObject& asteroid,GameObject& spaceShip){ shipAsteroid(spaceShip,asteroid); } void asteroidStation(GameObject& asteroid,GameObject& spaceStation){ stationAsteroid(spaceStation,asteroid); } class UnknownCollision{ //不明天体碰撞时的异常类 public: UnknownCollision(GameObject& object1,GameObject& object2){ } }; typedef void (*HitFunctionPtr)(GameObject&,GameObject&); //指向碰撞函数的函数指针 typedef std::pair StringPair; //关联碰撞函数两个参数的动态类型 //函数表的类型:每项关联了碰撞函数两个参数的动态类型名和碰撞函数本身 typedef std::map HitMap; HitMap* initializeCollisionMap(); //初始化函数表 HitFunctionPtr lookup(std::string const& class1, std::string const& class2); //在函数表中查找需要的碰撞函数 } //end namespace void processCollision(GameObject& object1,GameObject& object2){ ////根据参数的动态类型查找相应碰撞函数 HitFunctionPtr phf=lookup(typeid(object1).name(),typeid(object2).name()); if(phf) phf(object1,object2); //调用找到的碰撞处理函数来进行碰撞处理 else throw UnknownCollision(object1,object2); //没有找到则抛出异常 } namespace{ HitMap* initializeCollisionMap(){ //创建并初始化虚函数表 HitMap *phm=new HitMap; //创建函数表 //初始化函数表 (*phm)[StringPair(typeid(SpaceShip).name(), typeid(SpaceStation).name())]=&shipStation; (*phm)[StringPair(typeid(SpaceShip).name(), typeid(Asteroid).name())]=&shipAsteroid; (*phm)[StringPair(typeid(SpaceStation).name(), typeid(Asteroid).name())]=&shipAsteroid; //要包含所有的碰撞函数 //... (*phm)[StringPair(typeid(Asteroid).name(), typeid(SpaceStation).name())]=&asteroidStation; return phm; } } namespace{ //根据参数类型名在函数表中查找需要的碰撞函数 HitFunctionPtr lookup(std::string const& class1, std::string const& class2){ //用智能指针指向返回的函数表,为静态,表示只能有一个函数表 static std::auto_ptr collisionMap(initializeCollisionMap()); HitMap::iterator mapEntry=collisionMap->find(make_pair(class1,class2)); if(mapEntry==collisionMap->end()) return 0; //没找到,则返回空指针 return (*mapEntry).second; //找到则返回关联的碰撞函数 } } #endif

//GameTest.cpp:对游戏框架的测试 #include #include "GameObject.hpp" #include "Collision.hpp" int main(){ SpaceShip a; SpaceStation b; Asteroid c; processCollision(a,b); processCollision(a,c); processCollision(b,c); return 0; }

    解释:
    (1)各个碰撞处理函数的类型相同,都是void(GameObject&,GameObject&),因此在函数映射表中可以统一存放它们的指针。碰撞处理具有对称性,对称的版本直接交换一下参数来调用原来的版本即可。需要一个异常类,当没有找到对应的碰撞函数时,抛出异常。
    (2)把函数的两个参数的动态类型名称捆绑成pair对象,它的类型定义为StringPair,函数映射表的类型定义为HitMap。
    (3)主要有两个函数实现,在前面的匿名空间中进行了声明,然后在后面的匿名空间中进行了定义。一个初始化函数表initializeCollisionMap(),它创建实际的函数表,并把各个子类的名称和碰撞函数指针填入函数表中,返回函数表的指针。一个是查找碰撞函数指针的lookup(),它用静态的智能指针指向initializeCollisionMap()返回的函数表,表示创建唯一的一个函数。然后根据参数的动态类型名称查找函数表,找到则返回关联的碰撞函数指针。
    (4)这里使用了匿名的命名空间。匿名空间中所有的东西都局部于当前编译单元(本质上说就是当前文件),与其他文件中的同名实体无关系,它们的不同的实体。有了匿名命名空间,我们就无需使用文件作用域内的static变量(它也是局部于文件的),应该尽量使用匿名的命名空间。注意initializeCollisionMap()和lookup()在前面的匿名空间中声明了,因此后面的定义也必须放在匿名空间中,这样就保证了它们的声明和定义在同一编译单元内,链接器就能正确地将声明与本编译单元内的实现关联起来,而不会去关联别的编译单元内的同名实现。
    (5)全局的processCollision中,根据两个参数的动态类型名称查找函数表,找到接受此参数的函数指针,然后直接调用这个碰撞函数即可。
    (6)这里碰撞函数都是非成员函数。当增加新的GameObject子类时,原来的各个子类无需重新编译,也无需再维护一大堆的if/else。只需增加相应的碰撞函数,在initializeCollisionMap中增加相应的映射表项即可。
    2、函数表的改进。上面每增加一个碰撞函数时,都需要在initializeCollisionMap中静态地注册一个条目。我们可以把函数映射表的功能抽离出来,开发成一个独立的类CollisionMap,提供addEntry,removeEntry,lookup来动态地对函数表添加条目、删除条目、或者搜索指定的碰撞函数。我们还可以实现单例模式,让CollisionMap只能创建一个函数表。

//CollisionMap.hpp:碰撞处理函数的映射表,实现了单例模式 #ifndef COLLISION_MAP_HPP #define COLLISION_MAP_HPP #include #include //用到了pair及auto_ptr #include #include "GameObject.hpp" class CollisionMap{ //碰撞函数映射表 public: typedef void (*HitFunctionPtr)(GameObject&,GameObject&); //指向碰撞函数的函数指针 typedef std::pair StringPair; //关联碰撞函数两个参数的动态类型 typedef std::map HitMap; //函数表的类型 //根据参数类型名称在函数映射表中查找需要的碰撞函数 HitFunctionPtr lookup(std::string const& type1, std::string const& type2){ HitMap::iterator mapEntry=collisionMap->find(make_pair(type1,type2)); if(mapEntry==collisionMap->end()) return 0; //没找到,则返回空指针 return (*mapEntry).second; //找到则返回关联的碰撞函数 } //根据参数类型名称向映射表中加入一个碰撞函数 void addEntry(std::string const& type1, std::string const& type2, HitFunctionPtr collisionFunction){ if(lookup(type1,type2)==0) //映射表中没找到时插入相应条目 collisionMap->insert(make_pair(make_pair(type1,type2),collisionFunction)); } //根据参数类型名称从映射表中删除一个碰撞函数 void removeEntry(std::string const& type1, std::string const& type2){ if(lookup(type1,type2)!=0) //若找到,则删除该条目 collisionMap->erase(make_pair(type1,type2)); } private: std::auto_ptr collisionMap; //函数映射表,用智能指针存储 //构造函数声明为私有,以避免创建多个碰撞函数映射表 CollisionMap() : collisionMap(new HitMap){ } CollisionMap(CollisionMap const&); //不会调用,无需定义 friend CollisionMap& theCollisionMap(); }; inline CollisionMap& theCollisionMap(){ //返回唯一的一个碰撞函数映射表 static CollisionMap co; return co; } #endif

    解释:
    (1)CollisionMap的实现是很直接的,它维护一个collisionMap表来模拟虚函数表。碰撞函数的添加、删除、搜索都比较容易。theCollisionMap返回唯一的一个函数映射表。
    (2)现在游戏开发者就不再需要initializeCollisionMap、lookup这样的函数了,直接用theCollisionMap()来动态地添加和删除碰撞函数,在processCollision直接用theCollisionMap()来搜索给定索引的碰撞函数即可。可见,这种模拟虚函数表的方法还可以推广到多重分派的情况。

你可能感兴趣的:(C++语言&面向对象)