【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++

 

posted @ 2012-08-23 17:41  一点心青  阅读(654)  评论(0编辑  收藏  举报