多态的双重分派实现

来源:互联网 发布:域名怎么跳转代码 编辑:程序博客网 时间:2024/05/16 07:53

多态的双重分派实现
一般的多态是单重分派,即一个基类指针(或引用)直接到绑定到某一个子类对象上去,以获得多态行为。在前面“多态化的构造函数和非成员函数”介绍中,非成员函数函数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中,直接根据两个参数的动态类型名称查找函数表,找到接受此参数的函数指针,然后调用这个碰撞函数进行处理即可。
    下面是天体类的继承体系:
[cpp] view plaincopy
//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  
    下面是碰撞处理的实现:
[cpp] view plaincopy
//collision.hpp:碰撞处理  
#ifndef COLLISION_HPP  
#define COLLISION_HPP  
#include <string>  
#include <utility>   //用到了pair及auto_ptr  
#include <map>  
#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<std::string,std::string> StringPair; //关联碰撞函数两个参数的动态类型    
    //函数表的类型:每项关联了碰撞函数两个参数的动态类型名和碰撞函数本身  
    typedef std::map<StringPair, HitFunctionPtr> 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<HitMap> collisionMap(initializeCollisionMap());  
          
        HitMap::iterator mapEntry=collisionMap->find(make_pair(class1,class2));  
        if(mapEntry==collisionMap->end())  
            return 0; //没找到,则返回空指针  
        return (*mapEntry).second; //找到则返回关联的碰撞函数  
    }  
}  
          
#endif    
[cpp] view plaincopy
//GameTest.cpp:对游戏框架的测试  
#include <iostream>  
#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只能创建一个函数表。
[cpp] view plaincopy
//CollisionMap.hpp:碰撞处理函数的映射表,实现了单例模式  
#ifndef COLLISION_MAP_HPP  
#define COLLISION_MAP_HPP  
#include <string>  
#include <utility>   //用到了pair及auto_ptr  
#include <map>  
#include "GameObject.hpp"  
class CollisionMap{ //碰撞函数映射表  
public:   
    typedef void (*HitFunctionPtr)(GameObject&,GameObject&); //指向碰撞函数的函数指针    
    typedef std::pair<std::string,std::string> StringPair; //关联碰撞函数两个参数的动态类型    
    typedef std::map<StringPair, HitFunctionPtr> 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<HitMap> 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()来搜索给定索引的碰撞函数即可。可见,这种模拟虚函数表的方法还可以推广到多重分派的情况
0 0
原创粉丝点击