让函数根据一个以上的对象类型来决定如何虚化

本文探讨了在模拟太空游戏中的碰撞检测问题,通过三个版本的代码迭代,介绍了如何使用虚函数、函数指针和映射表来实现不同类型的碰撞处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本部分的代码来自于more effective Item 31,部分代码是我改的(很少)
一开始看见这个主题有点懵,没搞懂意思。看了书里的例子后发现这确实是一个很重要的问题

class GameObject{...};
class SpaceShip:public GameObject{...};
class SpaceStation:public GameObject{...};
class Asteroid:public GameObject{...};

void checkForCollision(GameObject& obj1,GameObject& obj2)
{
    if(theyJustCollided(obj1,obj2))
    {
        processCollision(obj1,obj2);
    }
    else
    {
        ...
    }
}

情景是模拟太空游戏中,不同天体的碰撞,看完这个流程后,发现调用processCollision需要两个object,需要知道这两个的类型,不然不好调用碰撞处理函数。这就需要依据两者的动态类型决定,单一的对象虚化是不够的。

版本一:

class SpaceShip;
class SpaceStation;
class Asteriod;

class GameObject {
public:
    virtual void collide(GameObject &otherObject) = 0;
};
class SpaceShip :public GameObject {
public:
virtual void collide(GameObject &otherObject);//碰撞函数的选择函数
virtual void hitSpaceShip(GameObject &otherObject);
virtual void hitSpaceStation(GameObject&otherObject);
virtual void hitAsteriod(GameObject&otherObject);

private:
typedef void (SpaceShip::*HitFunctionPtr)(GameObject&);//指向形参为(GameObject&)的函数指针
typedef map<string, HitFunctionPtr> HitMap;
static HitFunctionPtr lookup(const GameObject& whatWeight);//查找并返回碰撞函数
static HitMap *initializeCollisionMap();
};
void SpaceShip::hitSpaceShip(GameObject&otherObject) {
SpaceShip&otherObject = dynamic_cast<SpaceShip&>(otherObject);//将数据类型转回本身类型
}
void SpaceShip::hitSpaceStation(GameObject&otherObject) {
SpaceStation&otherObject = dynamic_cast<SpaceStation&>(otherObject);
}
void SpaceShip::hitAsteriod(GameObject&otherObject) {
Asteriod&otherObject = dynamic_cast<Asteriod&>(otherObject);
}
void SpaceShip::collide(GameObject &otherObject) {
HitFunctionPtr hfp = lookup(otherObject);
if (hfp)
(this->*hfp)(otherObject);
else
throw (1);
}
SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject&whatWeight) {
static auto_ptr<HitMap> collisionMap(initializeCollisionMap());
HitMap::iterator mapEntry = collisionMap->find(typeid(whatWeight).name());
if (mapEntry == collisionMap->end())return 0;
return (*mapEntry).second;
}
SpaceShip::HitMap *SpaceShip::initializeCollisionMap() {//使用map存储键值对,value对应函数
HitMap*phm = new HitMap;
(*phm)["SpaceShip"] = &hitSpaceShip;
(*phm)["SpaceStation"] = &hitSpaceStation;
(*phm)["Asteriod"] = &hitAsteriod;
return phm;
}
class SpaceStation :public GameObject {

};
class Asteriod :public GameObject {

};

缺点:
这种方法在如果加入新的类型,需要修改class的定义,意味着所有的客户都必须重新编译,甚至不在乎新型对象的人,这个操作和单虚函数的版本和if-else版本没多少差距,甚至不如他俩直接,所以接下来要做的就是将碰撞函数抽离出class让其成为非成员函数。

版本二:

class SpaceShip;
class SpaceStation;
class Asteriod;

class GameObject {
public:
    virtual void collide(GameObject &otherObject) = 0;
};
namespace {
    class SpaceShip :public GameObject {};
    class SpaceStation :public GameObject {};
    class Asteriod :public GameObject {};
    void shipAsteriod(GameObject &spaceShip, GameObject &asteriod) {//具体碰撞处理函数

    }
    void shipStation(GameObject&spaceShip, GameObject &spaceStation) {

    }
    void asteriodStation(GameObject&asteriod, GameObject&spaceStation) {

    }
    void asteriodShip(GameObject&asteriod, GameObject&spaceShip) {//两种情况结果应该一样
        shipAsteriod(spaceShip, asteriod);
    }
    void stationShip(GameObject&spaceStation, GameObject&spaceShip) {
        shipStation(spaceShip, spaceStation);
    }
    void stationAsteriod(GameObject&spaceStaion, GameObject&asteriod) {
        asteriodStation(asteriod, spaceStaion);
    }
    typedef void(*HitFunctionPtr)(GameObject &, GameObject&);//指向两个形参的指针函数
    typedef map<pair<string, string>, HitFunctionPtr> HitMap;
    pair<string, string>makeStringPair(const char *s1, const char *s2);//构造pair<string,string>
    HitMap*initializeCollisionMap();//HitMap初始化
    HitFunctionPtr lookup(const string&class1, const string&class2);//查找对应碰撞处理函数
}
void processCollision(GameObject&o1, GameObject&o2) {//调用碰撞处理函数
    HitFunctionPtr phf = lookup(typeid(o1).name(), typeid(o2).name());
    if (phf)phf(o1, o2);
    else throw(1);
}
namespace {
    pair<string, string>makeStringPair(const char *s1, const char *s2) {
        return pair<string, string>(s1, s2);
    }
}
namespace {
    HitMap*initializeCollisionMap() {
        HitMap*phm = new HitMap;
        (*phm)[makeStringPair("SpaceShip", "Asteriod")] = &shipAsteriod;
        (*phm)[makeStringPair("SpaceShip", "SpaceStation")] = &shipStation;
        (*phm)[makeStringPair("Asteriod", "SpaceStaion")] = &asteriodStation;
        return phm;
    }
}

namespace {
    HitFunctionPtr lookup(const string &class1, const string &class2) {
        static auto_ptr<HitMap>collisionMap(initializeCollisionMap());
        HitMap::iterator mapEntry = collisionMap->find(make_pair(class1, class2));
        if (mapEntry == collisionMap->end())return 0;
        return (*mapEntry).second;
    }
}

这个版本使用了匿名namespace,对于namespace内的每样东西对其所驻在的编译单元而言都是私有的。其效果就好像在文件里将函数声明为static一样。
缺点:
如果现在有一个Military Ship继承自SpaceShip,如果其与Asteriod相撞,我们期望调用
void shipAsteriod(GameObject&spaceShip,GameObject&asteroid);
但事实并未如此,会进行报错,因为这个函数针对的是SpaceShip和Asteriod,虽然是继承关系,但map里没有这条目录。
另一个缺点是无法对map进行新增,移除和修改等操作。一旦登陆了一个用来处理碰撞的函数,就永远停在那里了。

版本三:

class CollisionMap {
public:
    typedef void(*HitFunctionPtr)(GameObject &, GameObject&);
    void addEntry(const string &type1, const string &type2, HitFunctionPtr collisionFunction, bool symmetric = true){
        //直接对lookup函数进行操作,将这条目录加入到map中。
    }
    void removeEntry(const string &type1, const string&type2){
        //将这条pair信息从map中移除,操作:查找然后erase。
    }
    static CollisionMap &theCollisionMap() {
        static CollisionMap collisionMap; 
        return collisionMap;
    }
private:
    CollisionMap() {}
    CollisionMap(const CollisionMap&) {}

};
void shipAsteriod(GameObject &spaceShip, GameObject &asteriod) {

}
void shipStation(GameObject&spaceShip, GameObject &spaceStation) {

}
void asteriodStation(GameObject&asteriod, GameObject&spaceStation) {

}
void asteriodShip(GameObject&asteriod, GameObject&spaceShip) {
    shipAsteriod(spaceShip, asteriod);
}
void stationShip(GameObject&spaceStation, GameObject&spaceShip) {
    shipStation(spaceShip, spaceStation);
}
void stationAsteriod(GameObject&spaceStaion, GameObject&asteriod) {
    asteriodStation(asteriod, spaceStaion);
}
int main(void) {
    CollisionMap::theCollisionMap().addEntry("SpaceShip", "Asteriod", &shipAsteriod);

}

这个版本写的并不全,只是点到那个意思,把那些HitMap信息都封装在一个类里,然后通过外部接口对map进行添加,删除和修改,这只是对信息表的操作,并不是对整个碰撞实验。声明一点:这个过程只需要一个CollisionMap对象,所以采用了将缺省构造和拷贝构造私有化的操作,用外部接口调用构造函数实现构造的操作。

看完这三个版本,我是懵圈的,这东西很长,而且有的地方不好理解,但总的讲这部分觉得还是比较重要的,要不然也不会是四十多页的篇幅讲述这个问题,粗略的看这些东西采用的是自行模拟虚表的方法,至于方法是用map来存储键值对,通过map匹配响应函数,然后用函数指针调用相应的碰撞处理函数。

ps:还有一个小问题困扰了我,这完全是我的顺手失误,就是关于dynamic_cast 转型的问题,正常的做法是:

devired d;
base *p=d;
devired* ptr=dynamic_cast<devired*>(p);
devired &ptr=dynamic_cast<devired&>(*p);
结果我写成了
devired ptr=dynamic_cast<devired&>(*p);
怎么测都有问题,真的是很sui
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值