条款35 考虑virtual函数以外的其他选择
记住:
★virtual函数的替代方案包括NVI手法及Strategy模式的多种形式。NVI手法自身是一个特殊形式的Template Method模式
★将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员
★tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物
------------------------------------------------------------------------------------------
背景:
class GameCharacter {
public:
virtual int healthValue() const; //返回角色的健康指数
...
};
这是常规设计,以下探讨其替代解决方案。
借由non-virtual interface手法实现template method模式:
NVI手法对public virtual函数而言是一个有趣的替代方案。令客户通过public non-virtual成员函数间接调用private virtual函数,称为NVI手法。它是所谓的template method模式的一个独特表现形式(注意此处应将其与看过的设计模式知识联系起来!!!)。此public non-virtual函数称为virtual函数的外覆器(wrapper)。
class GameCharacter { public: int healthValue() const { //此即public non-virtual member 函数 ... //做一些事前工作 int retVal = doHealthValue(); //做真正的工作 ... //做一些事后的工作 return retVal; } ... private: virtual int doHealthValue() const { //派生类可以重新定义它!!! ... } };
注意点:
a NVI手法一个优点隐身在上述代码注释“做一些事前、事后工作”之中。事前工作可包括锁定互斥器、制造运转日志记录项、验证class约束条件、验证函数先决条件等;事后工作包括互斥器解锁、验证函数的事后条件、再次验证class约束条件等等。
b 注意一个C++规则:derived classes可重写继承而来的private virtual函数!!!!!!!
----------------
借由函数指针实现策略模式:(注意联系到essential C++笔记中的通过面向过程的方法来实现多态性的数列类例子!!!)
class GameCharacter; //以下函数是计算健康指数的缺省算法 int defaultHealthCalc( const GameCharacter &gc ); class GameCharacter { //类比于strategy模式的context类 public: typedef int (*HealthCalcFunc)( const GameCharacter & ); explicit GameCharacter( HealthCalcFunc hcf = defaultHealthCalc ) : healthFunc( hcf ) { } int healthValue() const { return healthFunc( *this ); } private: HealthCalcFunc healthFunc; //类比于context中的策略基类!!! };
此做法是常见的策略模式的简单应用。拿它和“基于GameCharacter继承体系内之virtual函数”的做法比较,提供了以下弹性:
♢ 同一人物类型之不同实体可以有不同的健康计算函数,如:
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用来替换当前的健康指数计算函数。
-------------------------------
借由tr1::function实现策略模式:
即对上面方法的改进:此处不使用function pointer,而是改用一个类型为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; };
如今GameCharacter持有一个tr1::function对象,相当于一个指向函数的泛化指针!!!此时使用起来弹性更大:
short calcHealth( const GameCharacter & ); //健康指数计算函数,注意其返回类型为non-int struct HealthCalculator { int operator()( const GameCharacter & ) const //为健康计算而设计的仿函数 {...} }; class GameLevel { public: float health( const GameCharacter & ) const; //成员函数,也可用来计算健康指数 ... //注意其non-int返回类型 }; class EvilBadGuy : public GameCharacter { //同前 ... }; class EyeCandyCharacter : public GameCharacter { //另一人物类型,假设其构造函数与 //EvilBadGuy相同 ... }; EvilBadGuy ebg1( calcHealth ); //人物1,使用其他函数计算健康指数 EyeCandyCharacter ecc1( HealthCalculator() ); //人物2,使用functer计算健康指数 GameLevel currentLevel; ... EvilBadGuy ebg2( std::tr1::bind( &GameLevel::health, currentLevel, _1 ) ); //人物3,使用某个成员函数计算健康指数,注意此语法形式!!!
------------
总结本条款提出的几个virtual函数的替代方案:
♢ 使用NVI手法,其以public non-virtual成员函数包裹较低访问性(private或protected)的virtual函数(这是模板方法模式的一种特殊形式)
♢ 将virtual函数替换为“函数指针成员变量”(这是策略模式的一种分解表现形式)
♢ 以tr1::function成员变量替换virtual函数 (这也是策略模式的某种形式)
条款36 绝不重写继承而来的non-virtual函数
记住:
★绝不重新定义继承而来的non-virtual函数
条款37 绝不重新定义继承而来的缺省参数值
记住:
★绝不重新定义继承而来的缺省参数值,∵缺省参数值都是静态绑定,而virtual函数---你唯一应该覆写的东西---却是动态绑定
----------------------------------------------------
本条款的讨论主题是“继承一个带有缺省参数值的virtual函数”。
class Shape { public: enum ShapeColor { Red, Green, Blue }; //Red是0 virtual void Draw( ShapeColor color = Red ) const = 0; ... }; class Rectangle : public Shape { public: //注意,赋予了不同的缺省参数值,糟糕!!! virtual void Draw( ShapeColor color = Green ) const; ... }; class Circle : public Shape { public: virtual void Draw( ShapeColor color ) const; //以上这么写则当客户以对象调用此函数,一定要指定参数值,∵静态绑定 //下这个函数并不从其base继承缺省参数值。但若以指针或引用调用此函数 //可不指定参数值,∵动态绑定下这个函数会从其base继承缺省参数值 ... }; Shape *pc = new Circle; Shape *pr = new Rectangle; //pr的静态类型是Shape*,动态类型是Rectangle*!!! pr->Draw(); //此处可不指定参数值,调用Rectangle::Draw( Shape::Red )
此处pr的动态类型是Rectangle*,∴调用的是Rectangle的virtual函数。Rectangle::Draw函数的缺省参数值应该是Green,但由于pr的静态类型是Shape*,∴此一调用的缺省参数值来自Shape class而非Rectangle class!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
--------------
若你试着遵循此条款,且同时提供缺省参数给base和derived class又会怎么样?
class Shape { public: enum ShapeColor { Red, Green, Blue }; //Red是0 virtual void Draw( ShapeColor color = Red ) const = 0; ... }; class Rectangle : public Shape { public: virtual void Draw( ShapeColor color = Red ) const; ... };
这就代码重复了!!!更糟糕的是代码重复又带来了相依性:若Shape内的缺省参数值改变了,所有“重复给定缺省参数值”的那些derived classes也必须改变!!!
--------------------------
当你想令virtual函数表现出你所想要的行为但却遭遇麻烦,聪明的做法是考虑替代设计:
如NVI手法:
class Shape { public: enum ShapeColor { Red, Green, Blue }; //Red是0 void Draw( ShapeColor color = Red ) const { //wrapper!!! doDraw( color ); } ... private: virtual void doDraw( ShapeColor color ) const = 0; }; class Rectangle : public Shape { public: ... private: virtual void doDraw( ShapeColor color ) const; ... };
由于non-virtual函数绝不会被derived classes覆写,这个设计很清楚地使得draw函数的color缺省参数值总是为Red。
条款38 通过复合塑模出has-a或“根据某物实现出”
记住:
★复合的意义与public继承完全不同
★在应用域复合意味has-a;在实现域符合意味is-implemented-in-terms-of(根据某物实现出)
条款39 明智而审慎地使用private继承
记住:
★private继承意味is-implemented-in-terms-of。它通常比复合的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这个设计是合理的
★和复合不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要
-----------------------------------------------------------------------------------------------------
private继承的两个典型特点:
1 若class之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象:
class Person {...}; class Student : private Person {...}; void eat( const Person &p ); Person P; Student s; eat( p ); //对 eat( s ); //错误!!!
2 基类中的public和protected成员private继承来之后全变private
-------------------------------------
private继承和复合一样意味is-implemented-in-terms-of。但它通常比复合的级别低,即原则上尽量使用复合,必要时才使用private继承,这个必要体现在:空间方面的厉害关系
class Timer { public: explicit Timer( int tickFrequency ); virtual void onTick() const; ... }; class Widget : private Timer { private: virtual void onTick() const; ... };
设计时Timer和Widget并不是将其设计成一个继承体系,即不是is-a关系,我们只想在Widget中用到定时器的onTick而已,所以此处用public继承并不合适,∴选择了private继承。
此设计的一个替代方案如下(使用复合):
class Widget {
private:
class WidgetTimer : public Timer { //类中定义类,可以!!!
public:
virtual void onTick() const;
...
};
WidgetTimer timer; //复合!!!
...
};
下面讲一种必要情况下(即涉及空间利害关系)使用private继承比复合要好:
这个情况非常激进只适用于所处理的class不带任何数据(这样的class无non-static成员变量;无virtual函数,因为virtual函数会给对象带来vptr;也无virtual base classes):
class Empty { };
//此为复合方式
class HoldsAnInt {
private:
int x;
Empty e; //C++规定凡是独立(非附属)对象必须有非0大小
};
这种适用复合设计会有sizeof( HoldsAnInt ) > sizeof( int );
若采用private继承:
class HoldsAnInt : private Empty {
private:
int x;
};
则有sizeof( HoldsAnInt ) == sizeof( int )。这是所谓的EBO(empty base optimization;空白基类最优化)。这样的实践很少增加derived classes的大小,挺好的!!!
注:现实生活中所指的empty class并不真的是空,其往往含有typedefs、enums、static成员变量或non-virtual函数。
条款40 明智而审慎地使用多重继承(未看)
记住:
★多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要
★virtual继承会增加大小、速度、初始化(及赋值)复杂度等成本。若virtual base classes不带任何数据,将是最具实用价值的情况
★多重继承的确有正当用途。其中一个情节涉及“public继承某个interface class”和“private继承某个协助实现的class”的两相组合