zoukankan      html  css  js  c++  java
  • 【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++

     

  • 相关阅读:
    从零开始,使用python快速开发web站点(1)
    实现ListView A~Z快速索引
    红黑树-Python实现
    折扣&折让-看清实质的思考
    【机器学习】初步理解:随机森林
    hdu-4611-Balls Rearrangement
    【经典算法】基本的排序算法:插入排序
    hdu 4620 Fruit Ninja Extreme(状压+dfs剪枝)
    【Java&Android开源库代码剖析】のAndroid-Universal-Image-Loader-part1
    从零开始,使用python快速开发web站点(2)
  • 原文地址:https://www.cnblogs.com/dwdxdy/p/2652875.html
Copyright © 2011-2022 走看看