【C++】让函数根据一个以上的对象来决定怎么虚拟
问题来源:假设正在编写一个小游戏,游戏的背景是发生在太空,有宇宙飞船、太空船和小行星,它们可能会互相碰撞,而且其碰撞的规则不同,如何用C++代码处理物体间的碰撞。代码的框架如下:
1 class GameObject{...}; 2 class SpaceShip:public GameObject{...}; 3 class SpaceStation:public GameObject{...}; 4 class Asteroid:public GameObject{...}; 5 6 void checkForCollision(GameObject& obj1,GameObject& obj2) 7 { 8 if(theyJustCollided(obj1,obj2)) 9 { 10 processCollision(obj1,obj2); 11 } 12 else 13 { 14 ... 15 } 16 }
正如上述代码所示,当调用processCollision()时,obj1和obj2的碰撞结果取决于obj1和obj2的真实类型,但我们只知道它们是GameObject对象。相当于我们需要一种作用在多个对象上的虚函数。这类型问题,在C++中被称为二重调度问题,下面介绍几种方法解决二重调度问题。
1.虚函数加RTTI
虚函数实现了一个单一调度,我们只需要实现另一调度。其具体实现方法:将processCollision()定义为虚函数,解决一重调度,然后只需要检测一个对象类型,利用RTTI来检测对象的类型,再利用if...else语句来调用不同的处理方法。具体实现如下:
1 class GameObject{ 2 public: 3 virtual void collide(GameObject& otherObject) = 0; 4 ... 5 }; 6 class SpaceShip:public GameObject{ 7 public: 8 virtual void collide(GameObject& otherObject); 9 ... 10 }; 11 12 class CollisionWithUnknownObject{ 13 public: 14 CollisionWithUnknownObject(GameObject& whatWehit); 15 ... 16 }; 17 void SpaceShip::collide(GameObject& otherObject) 18 { 19 const type_info& objectType = typeid(otherObject); 20 if(objectType == typeid(SpaceShip)) 21 { 22 SpaceShip& ss = static_cast<SpaceShip&>(otherObject); 23 process a SpaceShip-SpaceShip collision; 24 } 25 else if(objectType == typeid(SpaceStation)) 26 { 27 SpaceStation& ss = static_cast<SpaceStation&>(otherObject); 28 process a SpaceShip-SpaceStation collision; 29 } 30 else if(objectType == typeid(Asteroid)) 31 { 32 Asteroid& a = static_cast<Asteriod&>(otherObject); 33 process a SpaceShip-Asteroid collision; 34 } 35 else 36 { 37 throw CollisionWithUnknownObject(otherObject); 38 } 39 }
该方法的实现简单,容易理解,其缺点是其扩展性不好。如果增加一个新的类时,我们必须更新每一个基于RTTI的if...else链以处理这个新的类型。
2.只使用虚函数
基本原理就是用两个单一调度实现二重调度,也就是有两个单单独的虚函数调用:第一次决定第一个对象的动态类型,第二次决定第二个对象动态类型。其具体实现如下:
1 class SpaceShip; 2 class SpaceStation; 3 class Asteroid; 4 class GameObject{ 5 public: 6 virtual void collide(GameObject& otherObject) = 0; 7 virtual void collide(SpaceShip& otherObject) = 0; 8 virtual void collide(SpaceStation& otherObject) = 0; 9 virtual void collide(Asteroid& otherObject) = 0; 10 ... 11 }; 12 class SpaceShip:public GameObject{ 13 public: 14 virtual void collide(GameObject& otherObject); 15 virtual void collide(SpaceShip& otherObject); 16 virtual void collide(SpaceStation& otherObject); 17 virtual void collide(Asteroid& otherObject); 18 ... 19 }; 20 21 void SpaceShip::collide(GameObject& otherObject) 22 { 23 otherObject.collide(*this); 24 } 25 void SpaceShip::collide(SpaceShip& otherObject) 26 { 27 process a SpaceShip-SpaceShip collision; 28 } 29 void SpaceShip::collide(SpaceStation& otherObject) 30 { 31 process a SpaceShip-SpaceStation collision; 32 } 33 void SpaceShip::collide(Asteroid& otherObject) 34 { 35 process a SpaceShip-Asteroid collision; 36 }
与前面RTTI方法一样,该方法的缺点扩展性不好。每个类都必须知道它的同胞类,当增加新类时,所有的代码都必须更新。
3.模拟虚函数表
编译器通常创建一个函数指针数组(vtbl)来实现虚函数,并在虚函数被调用时在这个数组中进行下标索引。我们可以借鉴编译器虚拟函数表的方法,建立一个对象到碰撞函数指针的映射,然后在这个映射中利用对象进行查询,获取对应的碰撞函数指针,进行函数调用。具体代码实现如下:
1 namespace{ 2 void shipAsteroid(GameObject& spaceShip,GameObject& asteroid); 3 void shipStation(GameObject& spaceShip,GameObject& spaceStation); 4 void asteroidStation(GameObject& asteroid,GameObject& spaceStation); 5 ... 6 //implement symmetry 7 void asteroidShip(GameObject& asteroid,GameObject& spaceShip) 8 { shipAsteroid(spaceShip,asteroid);} 9 void stationShip(GameObject& spaceStation,GameObject& spaceShip) 10 { shipStation(spaceShip,spaceStation);} 11 void stationAsteroid(GameObject& spaceStation,GameObject& asteroid) 12 { asteroidStation(asteroid,spaceStation);} 13 14 typedef void(*HitFunctionPtr)(GameObject&,GameObject&); 15 typedef map<pair<string,string>,HitFunctionPtr> HitMap; 16 pair<string,string> makeStringPair(const char *s1,const char *s2); 17 18 HitMap* initializeCollisionMap(); 19 HitFunctionPtr lookup(const string& class1,const string& class2); 20 } 21 22 void processCollision(GameObject& obj1,GameObject& obj2) 23 { 24 HitFunctionPtr phf = lookup(typeid(obj1).name(),typeid(obj2).name()); 25 if(phf) 26 phf(obj1,obj2); 27 else 28 throw UnknownCollision(obj1,obj2); 29 } 30 31 namespace{ 32 pair<string,string> makeStringPair(const char *s1,const char *s2) 33 { 34 return pair<string,string>(s1,s2); 35 } 36 37 HitMap* initializeCollisionMap() 38 { 39 HitMap *phm = new HitMap; 40 (*phm)[makeStringPair("SpaceShip","Asteroid")] = &shipAsteroid; 41 (*phm)[makeStringPair("SpaceShip","SpaceStation")] = &shipStation; 42 ... 43 return phm; 44 } 45 46 HitFunctionPtr lookup(const string& class1,const string& class2) 47 { 48 static auto_ptr<HitMap> collisionMap(initializeCollisionMap()); 49 HitMap::iterator mapEntry = collisionMap->find(make_pair(class1,class2)); 50 if(mapEntry == collisionMap->end()) 51 return 0; 52 return (*mapEntry).second; 53 } 54 }
如上述代码所示,使用非成员函数来处理碰撞过程,根据obj1和obj2来查询初始化之后映射表,来确定对应的非成员函数指针。利用模拟虚函数表的方法,基本上完成了基于多个对象的虚拟化功能。但是为了更方便的使用代码,更方便的维护代码,我们还需要进一步完善其实现过程。
4.将映射表和注册映射表过程封装起来
由于具体应用的过程,映射表的映射关系存在着增加和删除的操作,因而需要把映射表封装类体,提供增加,删除等接口。具体实现如下:
1 class CollisionMap{ 2 public: 3 typedef void (*HitFunctionPtr)(GameObject&,GameObject&); 4 void addEntry(const string& type1,const string& type2,HitFunctionPtr collisionFunction,bool symmetric = true); 5 void removeEntry(const string& type1,const string& type2); 6 HitFunctionPtr lookup(const string& type1,const string& type2); 7 8 static CollisionMap& theCollisinMap(); 9 private: 10 CollisionMap(); 11 CollisinMap(const CollisionMap&); 12 };
在应用中,我们必须确保在发生碰撞前将映射关系加入了映射表。一个方法是让GameObject的子类在构造函数中进行确认,这将导致在运行期的性能开销,另外一个方法创建一个RegisterCollisionFunction类,用于完成映射关系的注册工作。RegisterCollisionFunction相应的代码如下:
1 class RegisterCollisionFunction{ 2 public: 3 RegisterCollisionFunction(const string& type1,const string& type2,CollisionMap::HitFunctionPtr collisionFunction,bool symmetric = true) 4 { 5 CollisionMap::theCollisionMap().addEntry(type1,type2,collisionFunction,symmetric); 6 } 7 }; 8 //利用此类型的全局对象来自动地注册映射关系 9 RegisterCollisionFunction cf1("SpaceShip","Asteroid",&shipAsteroid); 10 ...
参考资料:More Effective C++