最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!
假设你在制作一款游戏,游戏内的人物都有自己的生命值,而不同的人物会有不同的方式来计算它们健康指数,所以,需要将成员函数 healthValue()
声明为 虚函数 是非常正确的:
class GameCharacter {
public:
virtual int healthValue() const;
...
};
healthValue()
并未被声明为纯虚函数,这表示有一个计算健康指数的默认算法。
但是,这样做并不是最完美,它也有缺陷,所以,有没有其他方式来替代呢?
方法一:借由Non-Virtual Interface 手法实现 Template Method模式
有一个流派主张虚函数应该几乎总是 private。所以,他们认为保留 healthValue()
为 public,但让它成为普通函数,并调用一个 处于 private 域的虚函数来进行实际工作:
class GameCharacter {
public:
int healthValue() const { // 派生类 不重新定义它
... // 做一些事前工作
int retVal = doHealthValue(); //做真正的工作
... // 做一些事后工作
return retVal;
}
...
private:
virtual int doHealthValue() const { // 派生类可以重新定义它
... // 默认算法,计算健康指数
// 这里是隐式inline,但只是为了方便观看,inline与本条款无关
}
};
让用户通过 public 域的普通成员函数间接调用 private 域的虚函数,这种方法称作 non-virtual interface(NVI),也叫作 Template Method 设计模式(与C++模板并无关联)的一个独特表现形式。我们把普通函数(healthValue
)称为虚函数的外覆器(wrapper)。
NVI 的优点是在虚函数做真正的事情前后都能够做一些处理(如上述代码中的“事前工作”和“事后工作”)。这意味外覆器 确保虚函数被调用之前设定好适当场景,并在调用结束之后清理场景。“事前工作”有锁定互斥器、生成日志记录项、验证 class 约束条件、验证函数先决条件等;“事后工作”有互斥器解除锁定、验证函数的事后条件、再次验证 class 约束条件等。
但是此手法涉及在派生类中重新定义private 域的虚函数,即重新定义若干个派生类并不调用的函数(private 域的成员只能在自己类内使用)。NVI方法允许派生类重新定义虚函数(即决定如何去实现);但基类有着函数何时被调用的权利。
在 NVI 方式下其实没必要让虚函数一定是 private 。在一些类继承体系中要求派生类在虚函数实现内必须调用其基类的对应兄弟(即基类中对应的虚函数),为了让其合法,就不得不让虚函数为 protected ,不能使 private。甚至有时要求虚函数处于 public(如具备多态的基类的析构函数,见条款7),但这样一来就无法实施 NVI 方法了。
方法二:通过 Function Pointers 实现 Strategy 模式
这个方法主张,健康指数的计算与人物类型无关,因此就不需要“人物”这个成分,完全可以让每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们利用该函数进行实际计算:
class GameCharacter; //GameCharacter类的前置声明,见条款31
int defaultHealthCalc(const GameCharacter& gc); //计算健康指数的函数
class GameCharacter {
public:
typdef int (*HealthCalcFunc) (const GameCharacter&); //defaultHealthCalc函数指针的别名
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {}
int healthValue() const {
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc; //函数指针
};
这个方法就是 Strategy 设计模式的简单应用。它相较于在人物类内设置虚函数提供了弹性:
-
同一人物类型的不同实体可以有不同的健康计算函数。例如:
class EvilBadGuy: public GameCharacter { public: explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) : GameCharacter(hcf) { ... } ... }; int loseHealthQuickly(const GameCharacter&); //健康指数计算函数1 int loseHealthSlowly(const GameCharacter&); //健康指数计算函数2 //相同类型的对象搭配不同的健康计算方式 EvilBadGuy ebg1(loseHealthQuickly); //掉血快 EvilBadGuy ebg2(loseHealthSlowly); //掉血慢
-
某已知的人物的健康指数计算函数可以在运行期变更。
例如
GameCharacter
可提供一个成员函数setHealthCalculator
来替换当前的健康指数计算函数。
但是它也存在局限性。如果人物的健康根据该人物的 public 接口得来的信息加以计算就没问题,但是要用到 non-public 信息计算,就会出问题。实际上任何时候当你将类内的某个机能(也许是某个成员函数)替换为类外部的某个等价机能(也许是某个非成员非友元函数或另一个类的非友元函数)都会存在争议点。这个争议将持续至本条款其余篇幅,因为我们即将考虑的所有替代设计都涉及使用 GameCharacter
继承体系外的函数。
唯一能够解决“需要用非成员函数访问类的 non-public 成分” 的方法就是:弱化class的封装。例如 class 可声明那个非成员函数为 friend,或是为其实现的某一部分提供 public 访问函数。运用函数指针替换虚函数,其优点是否足以弥补缺点,是你必须根据每个设计情况的不同而抉择的。
方法三:通过 tr1::function 完成 Strategy 模式
一旦习惯通过模板以及它们对隐式接口(见条款41)的使用,基于函数指针的做法看起来便过分苛刻且死板了。
我们可以不用函数指针,而是用 tr1::function。就像条款54所说,这样的对象可持有(保存)任何可调用物(callable entity,也就是函数指针、函数对象或成员函数指针,方法二只能是函数指针),只要其签名式兼容于需求端。如用 tr1::function 替换刚才的设计:
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
//HealthCalcFunc可以是任何可调用物,可被调用并接受任何兼容于 GameCharacter 之物,返回任何兼容于 int 的东西
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {}
int healthValue() const {
return healthFunc( *this);
}
...
private:
HealthCalcFunc healthFunc;
};
HealthCalcFunc
是个 typedef,用来表现 tr1::function 的某个具体实体,意味该实体的行为像一般的函数指针。它的含义就是接受一个指向 GameCharacter
的引用,并返回 int。而上面所说的兼容的意思是这个可调用物的参数可被隐式转换为 const GameCharacter&
,返回类型可被隐式转换为 int 。
和 GameCharacter
持有函数指针的设计比较,这个设计几乎相同。唯一不同的是如今 GameCharacter
持有一个 tr1::function 对象,相当于一个指向函数的泛化指针。这个改变如此细小,我总说它没有什么明显的影响,除非用户在“指定健康计算函数”这件事上需要更惊人的弹性:
short calcHealth(const GameCharacter&);//健康计算函数,注意其返回类型为 not-int
struct HealthCalculator { // 为计算健康指数设计的成员对象
int operator() (const GameCharacter&) const {
...
}
};
class GameLevel {
public:
float health(const GameCharacter&) const; // 计算健康指数的成员函数,注意其返回类型为 not-int
...
};
class EvilBadGuy: public GameCharacter {
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) : GameCharacter(hcf) { ... }
...
};
class EveCandyCharacter: public GameCharacter {
... // 另一个人物类型,假设构造函数同EvilBadGuy
};
EvilBadGuy edg1(calcHealth); // 人物1,使用某个函数计算健康指数
EyeCandyCharacter ecc1(HealthCalculator()); // 人物2,使用某个函数对象计算健康指数
GameLevel currentLevel;
...
EvilBadGuy ebg2(std::tr1::bind(&GameLevel::health, currentLevel, _1)); // 人物3,使用某个成员函数计算健康指数
当我们使用 tr1::function 的时候会发现它能够做许多事。下面解释一下 tr1::bind
所发生的事情:
GameLevel::health
宣称它自己只接受一个参数(const GameCharacter&
),但它实际接受了两个参数,另一个参数是 GameLevel
,也就是 this 所指的那个。然而 GameCharacters
的健康计算函数只接受单一参数:GameCharacter
(这个对象被计算出健康指数)。
如果我们使用 GameLevel::health
作为 ebg2
的健康计算函数,我们必须以某种方式转换它,使它不再接受两个参数(一个 GameCharacter
和 一个 GameLevel
)而是接受一个单一参数(一个 GameCharacter
)。而在这个例子中 currentLevel
必然是 为“ebg2
的健康计算函数所需要的那个 GameLevel
对象”,于是我们将 currentLevel
绑定为 GameLevel
对象
这里在 ebg2
中第三个参数是_1,它意味着 当为 ebg2
调用 GameLevel::health
时,是通过 currentLevel
作为 GameLevel
对象。
但是,这不是重点,重点是,通过 tr1::function 替换函数指针,就可以允许用户在计算人物健康指数时使用任何兼容的可调用物。
方法四:古典的 Strategy 模式
古典的 Strategy模式会将健康计算函数做成一个分离的继承体系中的虚成员函数,看下面的UML图:
对应的代码如下:
class GameCharacter; //前置声明
class HealthCalcFunc {
public:
...
virtual int clac(const GameCharacter& gc) const { ... }
...
};
HealthCalcFunc defaultHealthCalc; //默认计算健康指数
class GameCharacter {
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc) : pHealthCalc(phcf) {}
int healthValue() const {
return pHealthCalc->calc(*this);
}
...
private:
HealthCalcFunc* pHealthCalc;
};
这个方法对于熟悉标准Strategy模式的人易理解使用,而且它还提供了 将一个既有的健康算法纳入使用的可能性。也就是在HealthCalcFunc
继承体系中添加派生类。
Note:
- virtual函数的替代方案包括 NVI 方法 及 Strategy设计模式的多种形式。NVI 方法 自身是一个特殊形式的 Template Method 设计模式
- 将机能从成员函数移到 class 外部函数,带来的一个缺点是非成员函数无法访问 class 内的 non-public 成员
- tr1::function 对象的行为就像一般的函数指针。这样的对象可接纳“与给定的目标签名式兼容”的所有可调用物