条款35、考虑虚函数以外的选择

本文深入探讨了虚函数的几种替代方案,包括NVI、FunctionPointer、tr1::function和传统Strategy模式的应用。通过具体示例展示了如何在不同场景下灵活替换虚函数,并分析了每种方案的优缺点。
	假设一个人物类,它有一个表现健康程度的的成员函数heath,返回一个数值表示人物健康程度。不同人物可能有不同的计算健康程度的方法,将其声明为虚函数是再明白不过的方法:
class Character
{
public:
        virtual int heath () const;  //返回健康指数,派生类可重载
…
}

health方法未声明为纯虚,暗示有计算健康指数的缺省方法。这个设计明白不过,我们可能会因此就没考虑其他方法。让我们考虑其他替代方案:

1、NVI手法实现Template Method模式
	从一个有趣的思想开始:这思想主张虚函数几乎是虚有的。较好的设计是保留health为公有函数,但是是非虚的。并调用一个私有虚函数(如doHealth)实际工作,如下
class Character
{
public:
         int heath() const  //派生类不重新定义它
         {
                   ….   //做一些事前工作
                            intval=doHealth();  //真正工作
                   ...    //做一些事前工作
                            
                            returnval; 
         }
private:
         virtualdoHealth() const    //派生类可重新定义
         {
                   ….     //缺省计算方法
         }
}

上述代码中成员函数本体位于类中只是便于阅读(虽然暗自成为inline),但与inline并无关联。
上述设计:通过公有非虚函数调用私有虚函数。称为NVI方法,non-virtual interface.是所谓设计模式Template Method设计模式(与C+模版无关)的一个独特表现模式。这个非虚函数health称为虚函数的外覆器(wrapper).
NVI的优点:事前事后工作保证在虚函数进行真正工作之前或之后调用。能确保在虚函数调用之前设定好场景,调用结束之后清理场景。事前工作如锁定互斥器,验证类约束条件等,事后工作如解除互斥锁定,验证函数事后条件等。这些都是直接调用虚函数所不能做到的。
NVI手法涉及在派生类中重新定义若干private virtual函数。(重新定义若干派生类并不调用的虚函数),这并不矛盾。重新定义虚函数表示事情“如何”完成,而调用虚函数表示“何时”完成。这些事情各自独立,NVI允许派生类重新定义虚函数,赋予“如何实现”的能力,但基类保留“函数何时调用”的权利。这看似奇怪,但“派生类可重新定义继承而来的prvate virtual函数”合情合理。
当然,NVI也没必要一定是虚有的,具体视情况而定。如多态性质的虚析构必须是public

	2、Function Pointer实现Strategy模式
	NVI虽然是一个不错的替代方案,但还是得用虚函数计算每个人的指数。另一个方法是:人物的健康指数和人物无关。完全不需要“人物”这个成分。如人物构造函数接受一个指针,指向一个健康函数。然后调用函数进行实际计算:
class Character; //前置声明
int defaultHealthCal(const Character& c);  //缺省计算方法
class Character
{
         typedef int(*healthCalF)(const Character&);
         public:
         explicit  Character(healthCalF fn=defaultHealthCal):healthc(fn);
         inthealthValue() const
         {
                   return healthc(*this);
         }
         ....
         private:
                   healthCalF healthc;
};
typedef int (*MYFUN)(int, int); 

这种用法一般用在给函数定义别名的时候
上面的例子定义MYFUN 是一个函数指针, 函数类型是带两个int 参数, 返回一个int

这个做法是常见Strategy设计模式的简单应用,提供了有趣弹性,如:
a、同一人物类型的不同实体可以有不同的计算方法。
class  Guy: public Character
{
         public:
         explicit  Guy(healthCalF  fn=defaultHealthCal):Character(fn)
{….}
         ….
};
int loseHealthQuickly(const Character&);
int loseHealthLowly(const Character&);
Guy g1(HealthQuickly);
Guy g2(loseHealthLowly);

b、某已知人物的健康计算函数在运行期间可变更。如Character可提供一个成员函数SetHealthCalcoulator来替换当前的计算函数。
换句话说计算函数不再是继承体系内的成员函数。如defaultHealthCal并未访问Guy的内部成分。如果计算信息可以根据人物pubic信息来计算则没问题,反之就不行了。实际上任何时候把一个类内的某机能替换为类外的某等价机能都是争议点。
一般唯一能解决“以非成员函数”访问类非公有成分的办法是弱化封装。如友元。或为其实现某部分提供公有访问函数。或用函数指针替换虚函数。

c、tr1::function完成Strategy模式
	为什么健康计算一定得是函数,而不能是“像函数的东西”(如函数对象)?如果是,为什么不能是成员函数?为什么一定是int返回,而不是可转换为int的类型?
	如果tr1::function对象而不是函数指针,这些约束就可解除。这样对象可以持有任何可调用物(函数指针,成员对象,成员函数指针等)。只要其签名式兼容于需求端。改变设计如下:
tr1::function:
class Character; //前置声明
int defaultHealthCal(const Character& c);  //缺省计算方法
class Character
{
// healthCalF可以是任何“可调用物”,可被调用并兼容任何兼容于Character之物,返回
//任何兼容于int的东西
         typedefstd::tr1::function<int (const Character&)> healthCalF;
         public:
         explicit  Character(healthCalFfn=defaultHealthCal):healthc(fn);
         inthealthValue() const
         {
                   return healthc(*this);
         }
         ....
         private:
                   healthCalF healthc;
};

上述tr1::function具体表现为一般函数指针。
tr1::function<int (const Character&)>
兼容性:可调用物参数可转换为const  Charater&,而返回值类型可隐式转换为int。
和前一个设计(函数指针)比较,持有一个tr1::function对象,相当于一个指向函数的泛化指针。虽改变小,但在“计算函数”上弹性惊人。如下:
short calHealth(const Character&);    //健康值计算函数,返回值为non-int
struct  HealthCalculator       //为计算健康而设计的函数对象
{  
         intoperator()(const Character&) const
{….}
};
class Level
{
public:
         floathealth (const Character&);  //计算健康的成员函数,返回值为float
…..
}
class Guy:public Character   //同前
{……};
class Guy1 public Character
{….};
Guy  g(calHealth);    //人物1,使用某个函数计算
Guy1 g1(HealthCalculator);   //人物2,使用某个函数对象计算
Level  leve;
Guy g2(std::tr1::bind(&Level::health,level,_1));   //人物3,使用某个成员函数计算


要计算g2,应使用Level的成员函数health,Level::health宣称接受一个参数(引用指向Character),但实际有两个参数,它也获得一个隐式Level,即this所指。但计算函数只接受Character一个参数。这样就需要转换。tr1::bind:绑定当前对象leve为Level对象,让“每次调用Level::health计算g2健康”时使用。“_1”意味着g2调用health以当前对象leve作为Level对象。
从上面可以看到:以tr1::function替换函数指针,在计算健康指数时允许使用任何兼容的可调用物。

d、古典的Strategy模式
传统做法会将计算函数设计为分离继承体系中的虚成员函数。如下:
class Character;  //前置声明
class HealthCalcF
{
         pubilc:
                   virtualint cal(const Character& c)const
                            {....}
         ...
};
HealthCalcF defaultHealthCal;
class Character
{
         public:
                   explicitCharacter(HealthCalcF* phcf=&defaultHealthCal):phealthCalF(phcf);
                   inthealthValue()
         {
                            returnphealthCalF->cal(*this);
         }
         ....
         private:
                   healthCalcF*phealthCalF;
};

这个方法的好处在于:熟悉标准Strategy的人很容易辩认,还提供“将既有算法纳入”的可能性。

总结,虚函数的几个替代方案
	1、NVI,template method设计模式的特殊形式。以公有非虚函数包裹较低访问性(private或protected)的虚函数	2、虚函数替换为“函数指针成员变量”,是strategy的一种分解表现形式。
	3、tr1::function成员变量替换虚函数,从而允许任何可调用物搭配一个兼容于需求的签名式。也是strategy的某种形式。
	4、将继承体系的虚函数替换为另一继承体系内的虚函数。strategy的传统实现方法。
上述虽未列出全部,但也说明了的确有不少替换方案。

需要记住的:
1、将机能从成员函数移到类外部函数,缺点是非成员函数无法访问类的非公有成员。
2、tr1::function对象形为就如一般函数指针。这样的对象可接纳“与给定目标兼容”的可调用物。
 
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值