More Effective C++ 条款31 让函数根据一个以上的对象类型来决定如何虚化

1. 假设要编写一个发生在太空的游戏,其中有飞船(spaceship),太空站(space station)和小行星(ssteroid),使它们继承自一个抽象基类GameObject,整个继承体系像这样:

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

不同的对象相撞有不同的规则,处理碰撞的函数声明像这样:

void checkForCollision(GameObject& object1,GameObject& object2);

这就产生了一个问题,要处理object1和object2的碰撞,必须知道这两个引用的动态类型,但C++支持的虚函数只支持single-dispatch(虚函数调用常被称为"message dispacth"——消息分派,函数调用如果根据两个参数而虚化,就被称为double dispatch,multiple dispatch同理).以下就是自行实现C++ double dispatch的方法.

(注:CLOS(Common Lisp Object System)支持multi-method,即可以根据任意多的参数虚化的函数)

2. 虚函数+RTTI(运行时类型识别)

最朴素的方法是使用if-else语句结合RTTI,像这样:

class CollisionWithUnknownObject {
public:
    //处理不明撞击物时所抛出的异常
    CollisionWithUnknownObject(GameObject& whatWeHit);
    ...
};
void SpaceShip::collide(GameObject& otherObject)
{
    const type_info& objectType = typeid(otherObject);
    if (objectType == typeid(SpaceShip)) {
        SpaceShip& ss = static_cast<SpaceShip&>(otherObject);
        process a SpaceShip-SpaceShip collision;
    }
    else if (objectType == typeid(SpaceStation)) {
        SpaceStation& ss =static_cast<SpaceStation&>(otherObject);
        process a SpaceShip-SpaceStation collision;
    }
    else if (objectType == typeid(Asteroid)) {
        Asteroid& a = static_cast<Asteroid&>(otherObject);
        process a SpaceShip-Asteroid collision;
    }
    //处理不明撞击物
    else {
        throw CollisionWithUnknownObject(otherObject);
    }
}

这种方法表面上实现简单,但实际上很不便于维护:如果要加入新的类型,那么继承体系中每一个类的collide函数可能都需要添加处理新型碰撞的代码.而且使用RTTI实现double-dispatching,也将根据参数动态类型采取不同行为的负担加在了程序员身上.

3. 只使用虚函数

这种方法的基本思想是通过对虚函数collide的重载,将double dispatch以两个single dispatch实现,以SpaceShip为例:

class SpaceShip; // 前置声明
class SpaceStation;
class Asteroid;
class GameObject {
public:
    virtual void collide(GameObject& otherObject) = 0;
    virtual void collide(SpaceShip& otherObject) = 0;
    virtual void collide(SpaceStation& otherObject) = 0;
    virtual void collide(Asteroid& otherobject) = 0;
    ...
};
class SpaceShip: public GameObject {
public:
    virtual void collide(GameObject& otherObject);
    virtual void collide(SpaceShip& otherObject);
    virtual void collide(SpaceStation& otherObject);
    virtual void collide(Asteroid& otherobject);
    ...
};

其中,virtual void collide(GameObject& otherObject)的实现像这样:

void SpaceShip::collide(GameObject& otherObject)
{
    otherObject.collide(*this);
}

看起来有些像递归调用,其实并非这样,在该函数内部,*this实际上已经对应该函数的动态类型,因此otherObject调用的将不再是collide(GameObject& otherObject),而是

collide(SpaceShip& otherObject),collide(SpaceStation& otherObject),collide(Asteroid& otherobject),SpaceShip::collide的其它重载版本像这样:

void SpaceShip::collide(SpaceShip& otherObject)
{
    process a SpaceShip-SpaceShip collision;
}
void SpaceShip::collide(SpaceStation& otherObject)
{
    process a SpaceShip-SpaceStation collision;
}
void SpaceShip::collide(Asteroid& otherObject)
{
    process a SpaceShip-Asteroid collision;
}

这种方法不需要使用RTTI,但却有和RTTI一样的缺点:一旦有新的class假如,代码就必须修改——含入一个新的虚函数,这涉及到类定义得修改,然而修改类定义会引起包含这些类定义的文件的重新编译,在很多情况下成本相当大.

4.

1). 自行仿真虚函数表格

由编译器使用虚函数表实现动态绑定的策略启发,可以自行仿真一个虚函数表格,它保存类名和对应碰撞处理函数指针的"键-值"对,并进行类名到碰撞处理函数的映射,达到double dispatch的目的,修改SpaceShip的定义如下:

class GameObject {
public:
    virtual void collide(GameObject& otherObject) = 0;
    ...
};
class SpaceShip: public GameObject {
public:
    virtual void collide(GameObject& otherObject);
    virtual void hitSpaceShip(SpaceShip& otherObject);
    virtual void hitSpaceStation(SpaceStation& otherObject);
    virtual void hitAsteroid(Asteroid& otherobject);
    ...
};
void SpaceShip::hitSpaceShip(SpaceShip& otherObject)
{
    process a SpaceShip-SpaceShip collision;
}
void SpaceShip::hitSpaceStation(SpaceStation& otherObject)
{
    process a SpaceShip-SpaceStation collision;
}
void SpaceShip::hitAsteroid(Asteroid& otherObject)
{
    process a SpaceShip-Asteroid collision;
}

以上定义中,SpaceShip不再重载collide,且碰撞处理函数hitSpaceShip,htiSpaceStation,hitAeteroid的参数都为GameObject&,这是由于不论用数组还是map容器还是其他方法,要把所有的碰撞处理函数指针放到同一个表内,就要求每个碰撞处理函数指针的类型相同,因此参数必须都为GameObject&,由于参数相同,因此也就不符合重载的要求.

可以再为SpaceShip定义一个lookup函数,进行类到相应碰撞处理函数的映射,lookup的声明像这样:

class SpaceShip: public GameObject {
private:
    typedef void (SpaceShip::*HitFunctionPtr)(GameObject&);
    //碰撞处理函数指针
    static HitFunctionPtr lookup(const GameObject& whatWeHit);
    ...
};

collide函数要使用lookup,像这样:

void SpaceShip::collide(GameObject& otherObject)
{
    HitFunctionPtr hfp =lookup(otherObject);
    if (hfp) {
        (this->*hfp)(otherObject); // call it
    }
    //处理未知碰撞
    else {
        throw CollisionWithUnknownObject(otherObject);
    }
}

现在就只剩下虚函数表和lookup的实现问题,虚函数表可以用STL内的map容器实现,它还要保证在第一次调用lookup函数时就已被初始化并在程序结束后被释放,比较好的选择是把它声明为lookup内的static对象,虚函数表和lookup的大体实现像这样:

class SpaceShip: public GameObject {
private:
    typedef void (SpaceShip::*HitFunctionPtr)(GameObject&);
    typedef map<string, HitFunctionPtr> HitMap;
    ...
};
SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit)
{
    static HitMap collisionMap;
    //查找
    HitMap::iterator mapEntry=collisionMap.find(typeid(whatWeHit).name());
    //未知碰撞
    if (mapEntry == collisionMap.end())
        return 0;
    return (*mapEntry).second;
}

2). 将自行仿真的虚函数表格(Virtual Function Table)初始化

现在面临collisionMap的初始化问题,由于collisionMap是函数内的static对象,因此像以下这样的初始化是不恰当的:

SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit)
{
    static HitMap collisionMap;
    collisionMap["SpaceShip"] = &hitSpaceShip;
    collisionMap["SpaceStation"] = &hitSpaceStation;
    collisionMap["Asteroid"] = &hitAsteroid;
    ...
}

它会造成每次调用lookup时都将hitSpaceShip,hitSpaceStation,hitAsteriod插入collisionMap内,要使得member function的插入动作只执行一次,可以将初始化动作提取到一个static 成员函数内,使用该static成员函数将collisionMap初始化,像这样:

class SpaceShip: public GameObject {
private:
    static HitMap initializeCollisionMap();
    ...
};
SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit)
{
    static HitMap collisionMap = initializeCollisionMap();
    ...
}

initializeCollisionMap使用按值传递,这意味着要付出构造和析构临时HitMap对象的成本,可以考虑使用标准库auto_ptr智能指针:

class SpaceShip: public GameObject {
private:
    static HitMap * initializeCollisionMap();
    ...
};
SpaceShip::HitFunctionPtrSpaceShip::lookup(const GameObject& whatWeHit)
{
    static auto_ptr<HitMap> collisionMap(initializeCollisionMap());
    ...
}

initializeCollisionMap的实现像这样:

SpaceShip::HitMap * SpaceShip::initializeCollisionMap()
{
    HitMap *phm = new HitMap;
    //正是为了以下操作,之前collide才放弃重载以使得hitSpaceShip,hitSpaceStation,hitAsteriod可以有相同参数
    (*phm)["SpaceShip"] = &hitSpaceShip;
    (*phm)["SpaceStation"] = &hitSpaceStation;
    (*phm)["Asteroid"] = &hitAsteroid;
    return phm;
}

由于hitSpaceShip,hitSpaceStation,hitAsteriod的参数是GameObject&,因此它们需要在函数内部运用dynamic_cast:

void SpaceShip::hitSpaceShip(GameObject& spaceShip)
{
    SpaceShip& otherShip=dynamic_cast<SpaceShip&>(spaceShip);
    process a SpaceShip-SpaceShip collision;
}
void SpaceShip::hitSpaceStation(GameObject& spaceStation)
{
    SpaceStation& station=dynamic_cast<SpaceStation&>(spaceStation);
    process a SpaceShip-SpaceStation collision;
}
void SpaceShip::hitAsteroid(GameObject& asteroid)
{
    Asteroid& theAsteroid =dynamic_cast<Asteroid&>(asteroid);
    process a SpaceShip-Asteroid collision;
}

3). 使用"非成员(non-member)函数"的碰撞处理函数

到此为止,仿真虚函数表的方法仍然不能解决2,3提出的策略共同的问题:当有新的class加入时,继承体系的每个类都需要添加处理新型碰撞的代码.这是因为此前的策略都是将处理碰撞的任务交由碰撞的某一方来执行,仿真虚函数表策略也不例外——每个class内含一个仿真的虚函数表,内含的指针也都指向成员函数.将碰撞处理函数设为non-member,就可以使得class定义式不包含碰撞处理函数,当需要添加碰撞处理函数时也就不需要修改class定义,自行设计的processCollision函数实现如下:

#include "SpaceShip.h"
#include "SpaceStation.h"
#include "Asteroid.h"
//使用匿名命名空间使得名称只对本单元可见,作用等同于将名称声明为static
namespace {
    void shipAsteroid(GameObject& spaceShip,GameObject& asteroid);
    void shipStation(GameObject& spaceShip,GameObject& spaceStation);
    void asteroidStation(GameObject& asteroid,GameObject& spaceStation);
    ...
    //对称碰撞
    void asteroidShip(GameObject& asteroid,GameObject& spaceShip){ shipAsteroid(spaceShip, asteroid); }
    void stationShip(GameObject& spaceStation,GameObject& spaceShip){ shipStation(spaceShip, spaceStation); }
    void stationAsteroid(GameObject& spaceStation,GameObject& asteroid){ asteroidStation(asteroid, spaceStation); }
    ...
    typedef void (*HitFunctionPtr)(GameObject&, GameObject&);
    typedef map< pair<string,string>, HitFunctionPtr > HitMap;
    pair<string,string> makeStringPair(const char *s1,const char *s2);
    HitMap * initializeCollisionMap();
    HitFunctionPtr lookup(const string& class1,
    const string& class2);
} //命名空间结束
void processCollision(GameObject& object1,GameObject& object2)
{
    HitFunctionPtr phf = lookup(typeid(object1).name(),typeid(object2).name());
    if (phf)
        phf(object1, object2);
    else
        throw UnknownCollision(object1, object2);
}

以上实现和之前有细微差异:HitFunctionPtr是一个typedef,表示指向non-member function的指针;exception class CollisionWithUnknownObject被重命名为UnKnownCollision并使用接受两个GameObject对象的构造函数;lookup必须接获两个GameObject名称并执行double dispatch.为了使得map的键含有两份信息,需要使用标准库pair类模板.makeStringPair,initializeCollisionMap,lookup的实现像这样:

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","Asteroid")] =&shipAsteroid;
        (*phm)[makeStringPair("SpaceShip", "SpaceStation")] =&shipStation;
        ...
        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;
    }
} 

由于makeStringPair,initializationCollisionMap,lookup都被声明于一个匿名namespace内,因此它们也必须实现于相同的namespace内,使得链接器能正确将其定义和声明关联.

通过将碰撞处理函数从类中分离,实现了即使新的GameObject被添加,原有的class也不需要重新编译,只需要在initializeCollisionMap中增加对应的键-值对,并在processCollision所在的匿名命名空间中申明一个新的碰撞处理函数即可.

4). "继承"+"自行仿真的虚函数表格"

自行仿真虚函数表格,并使用typeid通过类名匹配的方式进行查找的方法也有其缺点——它没有使用多态而是使用死板的名称字符串匹配来进行查找函数,结果是继承体系中的派生关系无法对函数匹配产生作用.假设继承体系中增加了商业飞船(CommercialShip)和军事飞船(MilitaryShip),令它们继承自抽象基类SpaceShip,整个继承体系像这样:

其中CommercialShip和MilitaryShip的碰撞行为完全相同,因此如果MilitrayShip和Asteroid碰撞,可能企图通过调用:

void shipAsteroid(GameObject& spaceShip,GameObject& asteroid);

来处理它们的碰撞,但这是错误的,因为lookup通过类名字符串匹配的方式查找对应函数,即使CommercialShip和MilitaryShip可被视为SpaceShip,lookup也无法得知.在此情况下,可能还是要回到3所提出的"双虚函数调用"机制.

5). 将自行仿真的虚函数表格初始化(再度讨论)

截至目前,整个设计是静态的:虚函数表在程序开始就生成,此后不再发生改变.但整个设计还有发挥空间:增加对仿真虚函数表做新增,删除,修改动作的功能.由于对虚函数表可以做多种操作,因此可以考虑定义一个CollisionMap类用于管理仿真虚函数表,并提供新增,删除,修改虚函数表的接口,CollisionMap的设计像这样:

class CollisionMap {
public:
    typedef void (*HitFunctionPtr)(GameObject&, GameObject&);
    void addEntry(const string& type1,const string& type2,HitFunctionPtr collisionFunction,bool symmetric = true);//symmetric用于标记是否产生两个顺序不同的碰撞处理函数,默认为true
    void removeEntry(const string& type1,const string& type2);
    HitFunctionPtr lookup(const string& type1,const string& type2);
    //产生唯一的CollisionMap
    static CollisionMap& theCollisionMap();
private:
    //构造函数声明为private从而限制CollisionMap只能产生一个
    CollisionMap();
    CollisionMap(const CollisionMap&);
};

对CollisionMap的使用像这样:

void shipAsteroid(GameObject& spaceShip,GameObject& asteroid);
CollisionMap::theCollisionMap().addEntry("SpaceShip","Asteroid",&shipAsteroid);
void shipStation(GameObject& spaceShip,GameObject& spaceStation);
CollisionMap::theCollisionMap().addEntry("SpaceShip","SpaceStation",
&shipStation);
void asteroidStation(GameObject& asteroid,GameObject& spaceStation);
CollisionMap::theCollisionMap().addEntry("Asteroid","SpaceStation",&asteroidStation);
...

目前为止仅剩的问题就是确保在碰撞发生之前对应的函数条目就被加入到了map之中,办法之一是令GameObject的subclass的constructor进行检查,这需要性能的开销,另一个方法就是使用一个RigisterCollisionFunction class进行管理:

class RegisterCollisionFunction {
public:
    RegisterCollisionFunction(const string& type1,const string& type2,CollisionMap::HitFunctionPtr collisionFunction,bool symmetric = true)
    {
        CollisionMap::theCollisionMap().addEntry(type1, type2,collisionFunction,symmetric);
    }
};

用户使用RigisterCollisionFunction进行"注册"就像这样:

RegisterCollisionFunction cf1(typeid(SpaceShip).name(), typeid(Asteroid).name(),&shipAsteroid);
RegisterCollisionFunction cf2(typeid(SpaceShip).name(), typeid(SpaceStation).name(),&shipStation);
RegisterCollisionFunction cf3(typeid(Asteroid), typeid(SpaceStation).name(),&asteroidStation);
...
int main(int argc, char * argv[])
{
    ...
}

由于这些全局对象在main函数调用之前就产生,因此它们的constructor所注册的条目也会在main被调用之前加入map,如果有新的derived class如Satellite和相应的碰撞函数产生:

class Satellite: public GameObject { ... };
void satelliteShip(GameObject& satellite,GameObject& spaceShip);
void satelliteAsteroid(GameObject& satellite,GameObject& asteroid);

函数可以使用类似方法加入到map之中而不需要改变原有代码:

RegisterCollisionFunction cf4(typeid(Satellite).name(),typeid(SpaceShip).name(),&satelliteShip);
RegisterCollisionFunction cf5(typeid(Satellite).name(),typeid(Asteroid).name(),&satelliteAsteroid);

注:C++标准并没有规定type_info::name的返回值,因此不同编译器会有不同实现,如有的编译器的name函数对于SpaceShip返回"class SpaceShip",因此需要用到类名的地方不能采取硬编码,应该统统使用typeid(classname).name(),否则在某些编译器下会因为classname!=typeid(classname).name()产生错误.

时间: 2024-10-13 22:38:05

More Effective C++ 条款31 让函数根据一个以上的对象类型来决定如何虚化的相关文章

More Effective C++ 条款12 了解”抛出一个exception&quot;与“传递一个参数”或“调用一个虚函数”之间的差异

1. 函数return值与try块throw exception.函数接收参数与catch字句捕获异常相当类似(不仅声明形式相像,函数参数与exception传递方式都有三种:by value,by reference , ). 2. 尽管函数调用与异常抛出相当类似,“从抛出端传递一个exception到catch子句”和“从函数调用端传递一个实参到被调函数参数”仍然大有不同: 1)调用一个函数,控制权会最终回到调用端(除非函数失败以致无法返回),但是抛出一个exception,控制权不会再回到

Effective C++ 条款25 考虑写出一个不抛出异常的swap函数

1. swap是STL的一部分,后来成为异常安全性编程(exception-safe programming)(见条款29)的一个重要脊柱,标准库的swap函数模板定义类似以下: namespace std{ template<typename T> swap(T& lhs,T& rhs){ T temp(lhs); lhs=rhs; rhs=temp; } } 只要T类型支持拷贝构造以及拷贝赋值,标准库swap函数就会调用T的拷贝构造函数和拷贝构造操作符完成值的转换,但对于某

Effective C++ -----条款31:将文件间的编译依存关系降至最低

支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式.基于此构想的两个手段是Handle classes 和 Interface classes. 程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在.这种做法不论是否涉及templates都适用.

effective c++ 条款17:以独立语句将newd对象置入智能指针

记住: 以独立语句将newd对象存储于智能指针内.如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏. int priority(); void processWidget(std::tr1::shared_ptr<Widget> pw, int priority); //编译错误,因为shared_ptr的构造函数是个explicit构造函数,无法进行隐式转换 processWidget(new Widget, priority()); //编译正确,但有潜在问题. //在调用pro

编写一个js函数,该函数有一个n(数字类型),其返回值是一个数组,该数组内是n个随机且不重复的整数,且整数取值范围是[2,32]

今天在公众号里边看到这个问题,就自己写了下,发现自己还是有许多没注意到的,在此记录, //返回一个随机整数 function getRandom( min , max ){ var rand = Math.round( Math.random() * (max - min) + min ); return rand; }; //重复性验证 function isRepeat(arr,n){ if (arr.indexOf(n) > -1) { return true; }; return fal

Effective C++ 条款11,12 在operator= 中处理&ldquo;自我赋值&rdquo; || 复制对象时不要忘记每一个成分

1.潜在的自我赋值     a[i] = a[j];     *px = *py; 当两个对象来自同一个继承体系时,他们甚至不需要声明为相同类型就可能造成别名. 现在担心的问题是:假如指向同一个对象,当其中一个对象被删,另一个也被删,这会造成不想要的结果. 该怎么办? 比如:   widget& widget:: operator+ (const widget& rhs) {    delete pd;    pd = new bitmap(*rhs.pb);    return *thi

effective c++条款13-17 “以对象管理资源”之C++隐式转换和转换构造函数

其实我们已经在C/C++中见到过多次标准类型数据间的转换方式了,这种形式用于在程序中将一种指定的数据转换成另一指定的类型,也即是强制转换,比如:int a = int(1.23),其作用是将1.23转换为整形1.然而对于用户自定义的类类型,编译系统并不知道如何进行转换,所以需要定义专门的函数来告诉编译系统改如何转换,这就是转换构造函数和类型转换函数! 注意:转换构造函数.隐式转换和函数对象不要搞混淆!!!函数对象是重载运算符(),和隐式转换函数易混淆. 一.转换构造函数 转换构造函数(conve

《Effective C++》之条款31:将文件间的编译依存关系降至最低

<Effective C++> 条款31:将文件间的编译依存关系降至最低 假设你对C++程序的某个class实现文件做了些轻微修改.注意,修改的不是class接口,而是实现,而且只改private成分.然后重新建置这个程序,预计只花数秒就好.毕竟只有一个class被修改.当你按下"Build"按钮或键入make指令时,会大吃一惊,然后感到困窘,因为你意识到整个世界都被重新编译个连接了!那么问题出在哪里呢??? 问题出在C++并没有把"将接口从实现中分离"

《Effective C 》资源管理:条款25--考虑写出一个不抛出异常的swap函数

条款25考虑写出一个不抛出异常的swap函数 条款25:考虑写出一个不抛出异常的swap函数 swap是STL中的标准函数,用于交换两个对象的数值.后来swap成为异常安全编程(exception-safe programming,条款29)的脊柱,也是实现自我赋值(条款11)的一个常见机制.swap的实现如下: namespace std{ template<typename T> void swap(T& a, T& b) { T temp(a); a=b; b=temp;