多态的双重分派实现
一般的多态是单重分派,即一个基类指针(或引用)直接到绑定到某一个子类对象上去,以获得多态行为。在前面“多态化的构造函数和非成员函数”介绍中,非成员函数函数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()来搜索给定索引的碰撞函数即可。可见,这种模拟虚函数表的方法还可以推广到多重分派的情况
一般的多态是单重分派,即一个基类指针(或引用)直接到绑定到某一个子类对象上去,以获得多态行为。在前面“多态化的构造函数和非成员函数”介绍中,非成员函数函数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()来搜索给定索引的碰撞函数即可。可见,这种模拟虚函数表的方法还可以推广到多重分派的情况