EC读书笔记系列之16:条款35、36、37、38、39、40

时间:2022-05-22 12:42:35

条款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 = ;
...
}; 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 = ;
...
}; 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 = ;
}; 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”的两相组合