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

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

  • 条款35考虑virtual函数以外的其他选择
    • 藉由Non-virtual Interface手法实现Template Method模式
    • 藉由Function Pointers实现Strategy模式
      • 藉由tr1function完成Strategy模式
      • 古典的Strategy模式
    • 摘要

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

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

    class GameCharacter{
    public:
        virtual int healthValue() 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:
        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手法了。

藉由Function Pointers实现Strategy模式

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函数比较,有更多弹性

  • 同一人物类型之不同实体可以有不同的健康计算函数。也就是说同一人物类型不同的对象可以有不同的健康计算,例如射击游戏中,一些购买防弹衣的玩家使用的对象,血量可以减少更慢。
  • 某已知人物健康计算函数可以在运行期间变更。即健康计算函数不再是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 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;
    };

在上面程序中,HealthCalcFunctypedef 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)。

古典的Strategy模式

《Effective C++》:条款35:考虑virtual函数以外的其他选择_第1张图片

在上面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函数寻找替代方案,有以下几个替代方案:

  • 使用non-virtual interface(NVI)手法,这是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数的包裹较低访问性(private或protected)的virtual函数。
  • 将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。
  • 以tr1::function成员替换virtual函数,这样可以允许任何可调用物(callable entity)搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。
  • 将继承体系内的virtual函数替换为另一个继承体系的virtual函数。这是Strategy设计模式的传统表现手法。

以上只是几种替换virtual函数的方案,并不是全部。此外还应该考虑各个方案的优缺点。

总结
- virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
- 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
- tr1::function对象行为就像一般函数指针。这样的对象有更大的弹性。

你可能感兴趣的:(C++,virtual,Strategy模式,高效C++,funciton对象)