《Effective C++》:条款35:考虑virtual函数以外的其他选择

条款35:考虑virtual函数以外的其他选择

virtual函数在派生中经常用到,在遇到一些问题时用virtual函数没问题,但是有时候我们应该思考一下是否有替代方案,以此来拓宽我们的视野。

假如现在正在写一个游戏,游戏中人物的血量随着战斗而减少,用一个函数healthValue返回这个血量值。因为不同人物血量值计算方法不同,所以应该讲healthValue声明为virtual:

class GameCharacter{public:() const;//derived classes可以重新定义……};

healthValue不是pure virtual,这暗示我们有个计算血量的缺省算法。(**条款**34)

这是个很明白清楚的设计,正是因为如此,我们可能没有考虑其他替代方案。我了跳出常规,我们来考虑一些其他解法。

藉由Non-virtual Interface手法实现Template Method模式

先看一个主张:virtual函数应该几乎总是private。这个主张建议,较好的设计是保留healthValue为public non-virtual成员函数,让它调用一个private virtual函数来做实际工作:

class GameCharacter{public:int healthValue() const{…… //做事前工作int retVal=doHealthValue();//真正做实际工作…… //做事后工作return retVal;}……private:() const //derived classes可以重新定义{……}};

这个设计是让客户通过public non-virtual成员函数间接调用private virtual函数,成为non-virtual interface(NVI)手法。它是所谓Template Method设计模式(与C++ templates没关系)的一个独特表现形式。这个non-virtual函数叫做virtual函数的外覆器(wrapper)。

NVI的优点在于“做事前工作”和“做事后工作”,这可以确保virtual函数在调用之前和调用之后做些工作,为virtual函数调动做准备,且在调用之后做些清理。例如事前工作可以包括锁定互斥器、制造日志记录项、验证class约束条件、验证函数先决条件等;事后工作可以包括解除互斥器、验证函数事后条件等。

这里有必要解释一下重新定义virtual函数和调用virtual函数。重新定义virtual函数表示某事任何被完成,调用virtual函数则表示何时完成某事。这并不矛盾。derived classes重新定义virtual函数,赋予它们如何实现的控制能力;但base class保留何时调用函数的权利。

NVI手法未必一定让virtual函数是private,某些classes继承体系要求derived class是protected。但是如果virtual函数是public(例如base classes的析构函数,**条款**7),这样就不能实施NVI手法了。

藉由Function Pointers实现Strategy模式

NVI使用了virtual函数来计算每个人的健康指数,还有一个主张:人物健康指数的计算和人物类型无关;这样计算不需要“人物”这个成分。例如可以在人物的构造函数接受一指针,指向健康计算函数:

GameChaaracter{public:typedef int(*HealthCalcFunc)(const GameCharacter&);explicit GameCharacter(HealthCalcFunc hcf=defaultHealthCalc):healthFunc(hcf){}int healthValue()const{ return healthFunc(*this); }……private:HealthCalcFunc healthFunc;//函数指针};

其实这是Strategy设计模式的应用。和使用virtual函数比较,有更多弹性

同一人物类型之不同实体可以有不同的健康计算函数。也就是说同一人物类型不同的对象可以有不同的健康计算,例如射击游戏中,一些购买防弹衣的玩家使用的对象,血量可以减少更慢。某已知人物健康计算函数可以在运行期间变更。即健康计算函数不再是GameCharacter继承体系内的成员函数。

实际上,class内的某个机能(也许是某个成员函数)替换为class的外部某个等价机能(例如某个non-member、non-friend函数或另个一class的non-friend成员函数),都有争议。

由外部函数访问内部成员时,有时需要弱化class的封装。这也就带来了缺点,但是优点(上面2点)是否足以弥补缺点,要视情况而定。

藉由tr1::function完成Strategy模式

如果习惯了templates以及它们对隐式接口(**条款**41)的使用,基于函数指针的实现便有点死板了。

可以不用函数指针,而是用一个类型为tr1::function的对象。这样的对象可以有(保存)任何可调用物(callable entity,即函数指针、函数对象、成员函数指针),只要其签名式兼容于需求端。用tr1::function实现

class GameCharacter;//forward declarationint defaultHealthCalc(const GameCharacter& gc);class GameCharacter{public:typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;explicit GameCharacter(HealthCalFunc hcf=defaultHealthCalc):healthFunc(hcf){}int healthValue() const{ return healthFunc(*this); }……private:HealthCalcFunc healthFunc;};

在上面程序中,HealthCalcFunc是typedef std::tr1::function<int (const GameCharacter&)>,其中尖括号中的内容是tr1::function具现体(instantiation)的目标签名式(target signature),签名代表的函数是“接受一个reference指向const GameCharacter,并返回int”。tr1::function类型(即HealthCalcFunc类型)产生的对象可以持有(保存)任何与此签名兼容的可调用物(callable entity)。兼容是指,这个可调用物的参数可被隐式转换为const GameCharacter&,且其返回类型可以被隐式转换为int。

和函数指针相比,这个设计只是把函数指针变成了tr1::function对象(相当于一个泛化的指针)。这个改变很小,但是提供了更大的弹性。例如

()(const GameCharacter&)const{……}};class GameLevel{public:float health(const GameCharacter&) const;//成员函数计算健康,返回不是int……};class EvilBadGuy:public GameCharacter{//和前面一样……};class EyeCandyCharacter: public GameCharacter{//另一个人物,假设其构造函数和EvilBadGuy相同……};//人物1,使用某个函数计算健康指数EvilBadGuy edg1(calcHealth);//人物2,,使用函数对象计算健康指数EyeCandyCharacter ecc1(HealthCalculator());GameLevel currentLevel;……//人物3,使用某个成员函数计算健康指数EvilBadGuy ebg2(std::tr1::bind(&GameLevel::health,currentLevel,_1));回避现实的人,未来将更不理想。

《Effective C++》:条款35:考虑virtual函数以外的其他选择

相关文章:

你感兴趣的文章:

标签云: