我是靠谱客的博主 迷路过客,最近开发中收集的这篇文章主要介绍Effective C++条款35:考虑 virtual 函数以外的其他选择(Consider alternatives to virtual functions)条款35:考虑 virtual 函数以外的其他选择总结,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

Effective C++条款35:考虑 virtual 函数以外的其他选择(Consider alternatives to virtual functions)

  • 条款35:考虑 virtual 函数以外的其他选择
    • 1、藉由Non-Virtual Interface手法实现Template Method模式
    • 2、藉由Function Pointers实现Strategy模式
    • 3、藉由tr1::function完成Strategy模式
    • 4、古典的Strategy模式
    • 5、四种替换方法总结
    • 6、牢记
  • 总结


《Effective C++》是一本轻薄短小的高密度的“专家经验积累”。本系列就是对Effective C++进行通读:

第6章:继承与面向对象设计

在这里插入图片描述


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

  你正在制作一个视频游戏,你打算为游戏中的人物设计一个继承体系。你的游戏属于暴力砍杀型,人类很容易受伤或者说健康度降低。因此你提供一个成员函healthValue,返回一个整型值来表明一个人物的健康程度。因为不同的人物会用不同的方式来计算健康程度,将healthValue声明为虚函数是再清楚不过的做法:

class GameCharacter {
public:
    //返回人物的健康指数,派生类可以重新定义它
    virtual int healthValue()const;
};

  healthValue没有被声明为纯虚函数的事实表明了会有一个缺省(默认)的算法来计算健康度(见条款34)。

  上面这个设计方案虽然可行,但是从某个角度来说却成了它的弱点。我们考虑下有没有别的方法来避免使用virtual函数。

1、藉由Non-Virtual Interface手法实现Template Method模式

  有的学派认为虚函数几乎应该总是private的。这个学派的信徒建议一个更好的设计方法是仍然将healthValue声明成public成员函数但是使其变为非虚函数,然后让它调用一个做实际工作的private虚函数,(例如doHealthValue)

class GameCharacter {
public:
    //派生类不应该重新定义它
    int healthValue()const {
        ...                         //事前工作,详下
        int retVal = doHealthValue();
        ...                          //事后工作,详下
        return retVal;
    }
private:
    //返回人物的健康指数,派生类可以重新定义它
    virtual int doHealthValue()const { 
    	...
    }
};
  • 条款30说过,成员函数在类中进行定义就会变为inline,但是此处不是,此处只是为了演示代码而已。

NVI手法特点

  • 通过一个 public 非虚接口隐藏掉虚接口具体的调用实例,并且可以灵活的按要求调用相关虚接口,这种方法称为 NVI (Non-Virtual Interface),这是设计模式的一个独特表现,我们将这个非虚函数称为虚函数的外覆器(wrapper)。

  • NVI手法的优点:我们可以在non-virtual函数中做一些其他事情。例如:

    • 事前工作:可以进行锁定互斥器、制造运转日志记录项、验证class约束条件、验证函数先决条件等等

    • 事后工作:可以进行互斥器解锁、验证函数的事后条件、再次验证class约束条件等等

  这些优点是在客户端直接调用virtual函数的情况中做不到的。

  NVI用法涉及到在派生类中重新定义private虚函数——重新定义它们不能调用的函数!这在设计上并不矛盾。重新定义一个虚函数指定如何做某事,而调用一个虚函数指定何时做某事。这些概念是相互独立的。NVI用法允许派生类重新定义一个虚函数,这使他们可以对如何实现一个功能进行控制,但是基类保有何时调用这个函数的权利。这看起来很奇怪,但是C++中的派生类可以重新定义继承而来的private虚函数的规则是非常明智的。

  NVI手法下并没有规定虚函数必须为private的。某些类的继承体系中,虚函数的派生类实现需要能够触发基类中对应的部分,如果使得这种调用是合法的,虚函数就必须为protected,而不是private的。有时一个虚函数甚至必须是public的(例如,多态基类中的析构函数——见条款7),但是这种情况下,NVI用法就不能够被使用了。

2、藉由Function Pointers实现Strategy模式

  NVI方法对public虚函数来说只是一个有趣的替代方案,虽然可以避免客户端直接调用virtual函数,但是在non-virtual函数中还是调用了virtual函数,这种方法还是没有免去定义virtual函数的情况。

  现在我们进行另一种设计,要求每个人物的构造函数接受一个指针,指向一个健康计算函数,我们可以调用该函数进行实际计算:

class GameCharacter;	// 前置声明
int defaultHealthCala(const GameCharacter& gc);//默认,计算健康指数
 
class GameCharacter {
public:
    //函数指针别名
    typedef int(*HealthCalcFunc)(const GameCharacter& gc);
    
    //构造函数
    explicit GameCharacter(HealthCalaFunc hcf = defaultHealthCalc) 
        :healthFunc(hcf) {}    
    int healthValue() {
        //通过函数指针调用函数
        return healthFunc(*this);
    }
    ...
private:
	HealthCalcFunc healthFunc; //函数指针
};

  这个做法是常见的策略模式的简单应用。同在GameCharacter继承体系中基于虚函数的方法进行对比,它能提供了某些趣弹性:

  • 同一个人物类型之间可以有不同的健康计算函数。例如:
class GameCharacter {
public:
    typedef int(*HealthCalcFunc)(const GameCharacter& gc);
    explicit GameCharacter(HealthCalaFunc hcf = defaultHealthCalc) 
        :healthFunc(hcf) {}    
    int healthValue() {
        return healthFunc(*this);
    }
    ...
private:
	HealthCalcFunc healthFunc;
};
class EvilBadGuy :public GameCharacter {
    explicit EvilBadGuy(HealthCalaFunc hcf = defaultHealthCalc)
        :GameCharacter(hcf) {}
	...
};
 
int loseHealthQuickly(const GameCharacter&);//健康指数计算函数1
int loseHealthSlowly(const GameCharacter&);//健康指数计算函数2

EvilBadGuy ebg1(loseHealthQuickly);//相同类型的人物搭配
EvilBadGuy ebg2(loseHealthSlowly);//不同的健康计算方式
  • 某已知人物的健康函数可在运行期变更。如:GameCharacter可提供一个成员函数setHealthCalculator,来替换当前的健康指数计算函数。

各种方法的不同选择:

  • 当全局函数可以根据class的public接口来取得信息并且加以计算,那么这种方法是没有问题的。但是如果计算需要访问到class的non-public信息,那么全局函数就不可以使用了。

  • 而解决上面的问题,唯一方法就是:弱化class的封装。例如将这个全局函数定义为class的friend,或者为其某一部分提供public访问函数

  因此,这些争议对于“以函数指针替换virtual函数”其是否利大于弊?取决于设计情况的不同。

3、藉由tr1::function完成Strategy模式

  一旦你适应了模板以及它们对隐式(implicit)接口的使用(见条款41),基于函数指针的方法看起来就过分死板了。为什么健康指数的计数必须是个函数而不能用行为同函数类似的一些东西来代替(例如,一个函数对象)?如果它必须是个函数,为什么不能是个成员函数?为什么必须返回int类型而不是能够转换成Int的任意类型呢?

  如果我们使用tr1::funciton对象来替换函数指针的使用,这些限制就会消失。如条款54所解释的,这些对象可以持有任何可调用实体(也就是函数指针,函数对象,或者成员函数指针),只要它们的签名同客户所需要的相互兼容。以下将刚才的设计改为使用tr1::function:

class GameCharacter;
 
int defaultHealthCala(const GameCharacter& gc);
 
class GameCharacter {
public:
    //同上
    //只是将函数指针改为了function模板,其接受一个const GameCharacter&参数,并返回int
    typedef std::tr1::function<int(const GameCharacter&)> HealthCalcFunc;	
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) 
        :healthFunc(hcf) {}
 
    int healthValue() {
        return healthFunc(*this);
    }
    ...
private:
    HealthCalcFunc healthFunc;
};

  如你所见,HealthCalcFunc是对一个实例化tr1::function的typedef。这意味着它的行为像一个泛化函数指针类型。看看HealthCalcFunc对什么进行了typedef:

std::tr1::function<int (const GameCharacter&)>

  这个tr1::function实例的“目标签名”(target signature)是“函数带了一个const GameCharacter&参数,并且返回一个int类型”。这个tr1::function类型的对象可以持有任何同这个目标签名相兼容的可调用实体。相兼容的意思意味着实体的参数要么是const GameCharacter&,要么可以转换成这个类型,实体的返回值要么是int,要么可以隐式转换成int。

  同上一个设计相比我们看到(GameCharacter持有一个函数指针),这个设计基本上是相同的。唯一的不同是GameCharacter现在持有一个tr1::function对象——一个指向函数的泛化指针。这个改动比较细小,但是结果是客户现在在指定健康计算函数上有了更大的弹性:

class GameCharacter { ... };
 
class EvilBadGuy :public GameCharacter {
public:
    explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
        :GameCharacter(hcf) {}
    ...
};
 
class EyeCandyCharacter :public GameCharacter {
    //构造函数类似EvilBadGuy 
};
 
//计算健康指数函数
short calcHealth(const GameCharacter&);
 
//函数对象,用来计算健康指数
struct HealthCalculator {
    int operator()(const GameCharacter&)const {}
};
 
//其提供一个成员函数,用以计算健康
class GameLevel {
public:
    float health(const GameCharacter&)const;
};
 

//人物1,其使用calcHealth()函数来计算健康指数
EvilBadGuy ebg1(calcHealth);

//人物2,其使用HealthCalculator()函数对象来计算健康指数
EyeCandyCharacter ecc1(HealthCalculator());

//人物2,其使用GameLevel类的health()成员函数来计算健康指数
GameLevel currentLevel;
EvilBadGuy ebg2(std::tr1::bind(&GameLevel::health, currentLevel, _1));

4、古典的Strategy模式

古典的Strategy做法会将计算健康的函数设计为一个分离的继承体系中的virtual成员函数,设计结果如下面的UML图:

在这里插入图片描述

UML的意义为:

  • GameCharacter是一个继承体系的根类,其派生类有EvilBadGuy、EyeCandyCharacter

  • HealthCalcFunc是一个继承体系的根类,其派生类有SlowHealthLoser、FastHealthLoser

  • 每一个GameCharacter对象都内含一个指针,指向于一个来自HealthCalcFunc继承体系中的对象

代码表示为:

class GameCharacter;
class HealthCalcFunc { //计算健康指数的类
public:
    virtual int calc(const GameCharacter& gc)const {}
};
 
HealthCalcFunc defaultHealthCalc;
 
class GameCharacter {
public:
    explicit GameCharacter(HealthCalcFunc* hcf = &defaultHealthCalc)
        :pHealthCalc(hcf) {}
 
    int healthValue() {
        return pHealthCalc->calc(*this);
    }
private:
    HealthCalcFunc* pHealthCalc;
};

  这个模式也具有弹性,它为现存的健康计算算法的调整提供了可能性,你只需要添加一个HealthCalcFunc的派生类就可以了。

5、四种替换方法总结

这个条款的基本建议是当为你所要解决的问题寻找一个设计方法时,考虑一下虚函数设计的替代方法。下面是我们介绍的设计方法回顾:

  • ① 使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性(private或protected)的virtual函数。

  • ② 将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。

  • ③ 以tr1::function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。

  • ④ 将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法。

6、牢记

  • virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。

  • 将机能从成员函数移到class外部函数,带来的一个缺点是:非成员函数无法访问class的non-public成员。

  • tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。

总结

期待大家和我交流,留言或者私信,一起学习,一起进步!

最后

以上就是迷路过客为你收集整理的Effective C++条款35:考虑 virtual 函数以外的其他选择(Consider alternatives to virtual functions)条款35:考虑 virtual 函数以外的其他选择总结的全部内容,希望文章能够帮你解决Effective C++条款35:考虑 virtual 函数以外的其他选择(Consider alternatives to virtual functions)条款35:考虑 virtual 函数以外的其他选择总结所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(44)

评论列表共有 0 条评论

立即
投稿
返回
顶部