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

     

  • 相关阅读:
    算法----(1)冒泡排序
    淘宝爬虫
    爬虫_豆瓣电影top250 (正则表达式)
    爬虫_猫眼电影top100(正则表达式)
    Android 简单调用摄像头
    Android 简单天气预报
    思维模型
    This view is not constrained, it only has designtime positions, so it will jump to (0,0) unless you
    Android studio preview界面无法预览,报错render problem
    Android studio 3.1.2报错,no target device found
  • 原文地址:https://www.cnblogs.com/dwdxdy/p/2652875.html
Copyright © 2011-2022 走看看