C/C++编程:考虑虚函数以外的其他选择

1059 篇文章 286 订阅

假设你正在写一个游戏软件,你将为你的游戏人物设计一个继承体系,每个任何都有一个健康值,因此你提供了一个成员函数healthValue表示任务的健康程度。由于不同的任务可能以不同的方式计算它们的健康指数,因此将healthValue()设计为virtual:

class GameCharacter{
public:
	virtual int healthValue() const; // 派生类可以重新实现它
};

health没有被声明为pure virtual,这暗示我们将有个计算健康指数的缺省算法。

这的确是个再明白不过的设计,但是从某个角度来说反而成为了它的弱点。由于这个设计如此明显,你可能因此没有认真考虑其他替代方案。为了帮助你跳脱面向对象设计路上的常轨,让我们考虑一些其他解法

借由non-virtual interface手法实现template method模式

让我们从一个有趣的思想流派开始,这个流派主张虚函数应该几乎总是private。这个流派的拥护者建议,较好的设计是保留healthValue的public成员函数,但让它成为一个non-vritual,并调用一个private virtual函数进行实际工作:

class GameCharacter{
public:
	int healthValue() const{
		// ...  做一些事前的工作
		int retVal = doHealthValue();
		// ... 做一些事后的工作
		return retVal ;
	}
	//...
private:
	virtual int doHealthValue() const{  // 派生类可以重新定义它
		//... 缺省算法
	}
}

注意:在这段代码中,直接在类定义中定义了成员函数本体,也就让它们全部暗自成为了inline。但是其实没有必要这样做的。所以不要认为成员函数在这里定义域类内由特殊用意。

这一基本设计,也就是”令客户通过public non-virtual成员函数间接调用private virtual函数“,成为non-virtual interface(NVI)手法。它是所谓的Template Method设计模式(与C++模板并无关联)的一个独特表现形式。我把这个non-vritual(healthValue)称为virtual函数的外覆器(wrapper)。

Template Method模式,模板方法模式。顾名思义,其定义就是在父类中定义处理流程的框架,在子类中实现具体的处理方式

NVI手法的一个优点是在”做一些事前工作“和"做一些事后工作"中。那些代码在”虚函数进行真正工作之前和之后“被调用。这意味着外覆器确保得以在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清理场景。"事前工作"可以包括锁定mutex、制造运转日志记录项、验证类约束条件、验证函数先决条件等待。"事后工作"可以包括解锁mutex、验证函数的事后条件、再次验证类约束条件等等。如果你让客户直接调用虚函数,就没有任何好办法可以做这些事。

NVI缺点是需要在派生类中重新定义private virtual函数,也就是说,重新定义若干个派生类并不调用的函数。NVI手法允许派生类重新定义虚函数,从而赋予它们”如何实现机能“的控制能力,但基类保留诉说”函数何时被调用的权力“。这听起来诡异,但是C++的派生类可以重写定义继承来的private virtual函数的规则使得它完全合情合理。

”重新定义虚函数“表示某些事”如何“被完成,”调用虚函数“则表示它”何时“被完成。

在NVI手法下其实没有必要让虚函数一定是private:

  • 某些类继承体系要求派生类在虚函数的实现必须调用其基类的对应兄弟,而为了让这样的调用合法,虚函数必须是protected,不能是private
  • 有时候虚函数一定是public(比如具体多态性质的基类的析构函数),这样一来就不能实施NVI手法了。

借由函数指针实现策略模式

有个设计主张”人物健康指数的计算与人物类型无关“,这样的计算完全不需要”人物“这个成分。比如我们可能会要求每个任务的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算。

class GameCharacter; // 前置声明
// 下面函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
	typedef int(*HealthCalcFunc)(const GameCharacter&);
	explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf){}
	int healthValue() const{
		return healthFunc(*this);
	};
private:
	HealthCalcFunc healthFunc;
};

这个做法是策略(Strategy)模式的简单应用。拿它和”植基于GameCharacter继承体系内的虚函数“的做法比较,它提供了某些有趣弹性:

  • 同一类型的不同实体可以由不同的健康计算函数,比如:
class EvilBadGuy : public GameCharacter{
public:
	explicit EvilBadGuy (HealthCalcFunc hcf = defaultHealthCalc) : EvilBadGuy (hcf) {
	}
};

int loseHealthCalc1(const GameCharacter& gc); //计算方法1
int loseHealthCalc2(const GameCharacter& gc); // 计算方法2

EvilBadGuy egb1(loseHealthCalc1);
EvilBadGuy egb2(loseHealthCalc2); //相同的人物类型,不同的方法计算
  • 某已知人物的健康计算函数可以在运行期变更。比如GameCharacter可以提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。

换句话说,”健康计算函数“不再是GameCharacter继承体系内的成员函数”。这意味着这些计算函数并未特别访问“即将被计算健康指数”的那个对象的内部成本,比如defaultHealthCalc并未访问EvilBadGuy 的non-public成本。

如果人物的健康可以纯粹根据该人物public接口来计算,那么就没有问题,但如果需要non-public信息进行精确计算,那就有问题了。

一般而言,唯一能够解决“需要以non-member函数访问类的non-public成分”的方法是:弱化类的封装。比如类可以声明那么非成员函数为友元,或者为其实现的某一部分提供public访问函数。

运用函数指针替换虚函数,其优点(“每个对象可以各自拥有自己的健康计算函数”、“可以在运行期改变计算函数“)是否足以弥补缺点(”可能必须降低封装性“),必须根据每个设计情况的不同而抉择。

借由function完成策略模式

一旦析构了模板以及它们对隐式接口的使用,基于函数指针的做法看起来就过分苛刻死板了:为什么要求”健康指数计算“必须是个函数,而不是是某种”像函数的东西“(比如函数对象)呢?如果一定需要是函数,为什么不能是成员函数呢?为什么一定要返回int而不是任何可被转换为int的类型呢?

如果我们不再使用函数指针(前面是healthFunc),而是改用一个类型为function的对象,这些约束就全部没有了。这样的对象可以持有任何可调用物(函数指针、函数对象、成员函数指针),只要其签名兼容于需求端:

class GameCharacter; // 前置声明
// 下面函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
	// HealthCalcFunc 可以是任何可调用物,可被调用并接受任何兼容于GameCharacter之物,返回任何兼容于int的东西
	typedef std::function<int(const GameCharacter&)>HealthCalcFunc ;
	explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf){}
	int healthValue() const{
		return healthFunc(*this);
	};
private:
	HealthCalcFunc healthFunc;

和前一个设计相比(GameCharacter持有的是函数指针),这个设计唯一不同的是现在GameCharacter持有一个function对象,相当于一个指向函数的泛化指针。泛化带来更大的弹性:

short CalcHealth(const GameCharacter& gc);
struct HealthCalculator{  //为计算健康而设计的函数对象
	int operator()(const GameCharacter&) const{...}
};

class GameLevel{
public:
	float health(const GameCharacter&) const; // 成员函数,用以计算健康
};

class EvilBadGuy : public GameCharacter{
public:
	explicit EvilBadGuy (HealthCalcFunc hcf = defaultHealthCalc) : EvilBadGuy (hcf) {
	}
};

class EyeCandyCharacter : public GameCharacter{
public:
	explicit EyeCandyCharacter (HealthCalcFunc hcf = defaultHealthCalc) : EvilBadGuy (hcf) {
	}
};

EvilBadGuy  ebg1(CalcHealth); // 人物1:使用每个函数计算健康指数
EyeCandyCharacter ecc1(HealthCalculator());  // 人物2:使用函数对象计算健康指数

GameLevel currentLevel;
EvilBadGuy   ebg2(std::bind(&GameLevel::health, currentLevel, _1));

关于:std::bind(&GameLevel::health, currentLevel, _1),调用了float GameLevel::health(const GameCharacter&) const;, 分析如下:

  • GameLevel::health宣称自己接受一个参数,但实际上它接受两个参数,因为它也获得一个隐式参数GameLevel ,也就是this所指的那个
  • 但是GameCharacter的typedef std::function< int(const GameCharacter&)>HealthCalcFunc宣称自己只接受一个参数GameCharacter
  • 我们必须以某种方式转换它,使它不再接受两个参数(GameLevel 和 GameCharacter),这就是bind所作的工作

简而言之:如果用function替换函数指针,将允许客户在计算人物健康指数时使用任何兼容的可调用物

传统的策略模式

传统(典型)的策略模式会将健康计算指数做成一个分离的继承体系中的虚函数成员:

  • GameCharactor是某个继承体系的根类,EviBadGuy和EyeCandyCharacter是它的派生类。
  • HealthCalcFunc 是另一个继承体系的根类,SlowHealthLoser和FastHealthLoser都是派生类
  • 每一个GameCharactor对象都内含一个指针,指向一个来自HealthCalcFunc 继承体系中的对象
    在这里插入图片描述
    下面是对应的代码骨干:
class GameCharactor; //前置声明
class HealthCalcFunc {
public:
	virtual int calc(const GameCharactor & gc) const;
};

HealthCalcFunc  defaultHealthCalc;
class GameCharactor{
public:
	explicit GameCharactor(HealthCalcFunc  *phcf = &defaultHealthCalc) :pHealthCalc(phcf ){}
	int healthValue() const {return pHealthCalc->calc(*this)}
private:
	HealthCalcFunc  * pHealthCalc;
};

总结

忠告:当你为解决问题寻找某个设计方案时,不妨考虑虚函数的替代方案。比如说:

  • 使用non-virtual interface(NVI)手法,这时template method设计模式的一种特殊形式。它以public non-vritual成员函数包裹较低访问性(priavte或者protected)的虚函数
  • 将虚函数替换为”函数指针变量“,是策略模式的一种表现形式
  • 以function变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的实现方式。这也是策略模式的一种表现形式
  • 将继承体系内的虚函数替换为另一个继承体系内的虚函数。这时策略模式的传统实现手法

请记住:

  • 虚函数的替代方案包括NVI手法(模板方法设计模式)以及策略设计模式的多种形式。
  • 将功能从成员函数移到类外部函数,带来的一个缺点是,非成员函数无法访问类的non-public成员
  • function对象的行为就像一般函数指针。这样的对象可以接纳”与给定的模板签名式兼容“的所有可调用物
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值