virtual函数在派生中经常用到,在遇到一些问题时用virtual函数没问题,但是有时候我们应该思考一下是否有替代方案,以此来拓宽我们的视野。
假如现在正在写一个游戏,游戏中人物的血量随着战斗而减少,用一个函数healthValue
返回这个血量值。因为不同人物血量值计算方法不同,所以应该讲healthValue声明为virtual:
class GameCharacter{
public:
virtual int healthValue() const;//derived classes可以重新定义
……
};
healthValue
不是pure virtual,这暗示我们有个计算血量的缺省算法。(**条款**34)
这是个很明白清楚的设计,正是因为如此,我们可能没有考虑其他替代方案。我了跳出常规,我们来考虑一些其他解法。
先看一个主张:virtual函数应该几乎总是private。这个主张建议,较好的设计是保留healthValue为public non-virtual成员函数,让它调用一个private virtual函数来做实际工作:
class GameCharacter{
public:
int healthValue() const
{
…… //做事前工作
int retVal=doHealthValue();//真正做实际工作
…… //做事后工作
return retVal;
}
……
private:
virtual int doHealthValue() 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手法了。
NVI使用了virtual函数来计算每个人的健康指数,还有一个主张:人物健康指数的计算和人物类型无关;这样计算不需要“人物”这个成分。例如可以在人物的构造函数接受一指针,指向健康计算函数:
class GameCharacter;//forward declaration
int defaultHealthCalc(const GameCharacter& gc);//健康计算缺省算法
class 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函数比较,有更多弹性
实际上,class内的某个机能(也许是某个成员函数)替换为class的外部某个等价机能(例如某个non-member、non-friend函数或另个一class的non-friend成员函数),都有争议。
由外部函数访问内部成员时,有时需要弱化class的封装。这也就带来了缺点,但是优点(上面2点)是否足以弥补缺点,要视情况而定。
如果习惯了templates以及它们对隐式接口(**条款**41)的使用,基于函数指针的实现便有点死板了。
可以不用函数指针,而是用一个类型为tr1::function的对象。这样的对象可以有(保存)任何可调用物(callable entity,即函数指针、函数对象、成员函数指针),只要其签名式兼容于需求端。用tr1::function实现
class GameCharacter;//forward declaration
int 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对象(相当于一个泛化的指针)。这个改变很小,但是提供了更大的弹性。例如
short calcHealth(const GameCharacter&);//健康计算函数,返回类型不是int
struct HealthCalculator{//为健康计算设计的函数对象
int operator()(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));
从上面例子可以看出,tr1::function
给我们带来的弹性远远大于函数指针。下面来看一下tr1::bind
发生的事。
计算ebg2的健康指数时,使用的是GameLevel class的成员函数health。GameLevel::health
表面上接受一个参数,但它实际上接受两个参数,它还接受了一个参数类型为GameLevel的currentLevel,即this指针。GameCharacter健康计算函数只接受一个参数GameCharacter,但GameLevel::health接受两个参数,因此使用了tr1::bind
。“_1”表示ebg2调用GameLevel::health时以currentLevel作为GameLevel对象。
通过上面例子可以看出,用tr1:;function
替换函数指针,将允许客户在计算人物健康指数时使用任何兼容的可调用物(callable entity)。
在上面UML图中,GameCharacter是某继承体系中的基类,EvilBadGuy和EyeCandyCharacter是derived classes。HealthCalcFunc是另一继承体系的基类,SlowHealthLoser和FastHealthLoser是derived classes;每个GameCharacter对象都还有指针,指向来自HealthCalcFunc继承体系的对象。
class GameCharacter;//forward declaration
class HealthCalcFunc{
public:
……
virtual int calc(const GameCharacter& gc) const
{ …… }
……
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter{
public:
explicit GameCharacter(HealthCalcFunc* phcf=&defaultHealthCalc)
:pHealthCalc(phcf)
{}
int healthValue() const
{ return pHealthClac->calc(*this); }
……
private:
HealthCalcFunc* pHealthCalc;
};
熟悉标准Strategy模式的人很容易辨认它,它还提供“将一个既有健康计算方法纳入使用”的可能性,只要为HealthCalcFunc继承体系添加一个derived class即可。
本条款主要讲述,为virtual函数寻找替代方案,有以下几个替代方案:
以上只是几种替换virtual函数的方案,并不是全部。此外还应该考虑各个方案的优缺点。
总结
- virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
- 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
- tr1::function对象行为就像一般函数指针。这样的对象有更大的弹性。