您的位置:首页 > 编程语言 > C语言/C++

Effective C++ —— 继承与面向对象设计(六)

2015-09-16 08:43 441 查看
条款32 : 确定你的public继承塑模出is-a关系

  以C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味“is-a”(是一种)的关系。请务必牢记。当你令class D 以public形式继承class B,你便是告诉C++编译器,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。你的意思是B比D表现出更一般化的概念,而D比B表现出更特殊化的概念。is-a关系只对public继承才成立,private继承的意义于此完全不同(条款39),至于protected继承,至今仍困惑着我。考虑如下例子:

class Person { ...  };
class Student : public Person { ... };


  每个学生都是人,但并非每一个都是学生。对人可以成立的每一件事,对学生也都成立。任何函数如果期望获得一个类型为Person(或pointer-to-Person 或 reference-to-Person)的实参,也都愿意接受一个Student对象(或pointer-to-Student或 reference-to-Student)。

  然而,考虑这种情况:企鹅是一种鸟,这是事实。鸟可以飞,这也是事实。如此,下面代码:

class Bird
{
public:
virtual void fly();    // 鸟可以飞
.....
};

class Penguin:public Bird  // 企鹅是一种鸟
{
.....
};


  有问题:这个继承体系说企鹅可以飞!!我们对上述代码作出如下修改(Method1):

class Bird
{
.....            // 没有声明fly函数
};
class FlyingBird:public Bird
{
public:
virtual void fly();
.....
};
class Penguin:public Bird  // 企鹅是一种鸟
{
.....    // 没有声明fly函数
};


  此刻,这样的继承体系较好的满足了我们的真正意思。然而,对某些系统而言,可能不需要区分会飞的鸟和不会飞的鸟,原先修改之前的“双classes继承体系”就已经满足需求了。这反映出一个事实:世界上并不存在一个“适用于所有软件”的完美设计。所谓最佳设计,取决于系统希望做什么事,包括现在与未来。

  对原先代码的令一种修改如下(Method2):

void error(const std::string& msg);     // 定义于另外某处
class Penguin:public Bird  // 企鹅是一种鸟
{
virtual void fly() { error("Attempt to make a penguin fly!"); }      // 运行期发生错误
.....
};


  注意:这里并不是说“企鹅不会飞”,而是说“企鹅会飞,但尝试那么做是一种错误”。Method2是在程序运行期发生错误,然而,我们知道,把错误提前到编译器是较佳的选择:

class Bird
{
........ // 没有声明fly函数
};
class Penguin:public Bird
{
........ // 没有声明fly函数
};
// 那么下面调用会使编译器报错
Penguin p;
p.fly();    // 错误


  is-a并非是唯一的classes之间的关系。另两个常见的关系是has-a(有一个)和is-implemented-in-terms-of(根据某物实现出)。这些关系将在条款38和39讨论。将上述这些重要的相互关系中的任何一个误塑为is-a而造成的错误设计,在C++中并不罕见,所以,你务必了解这些“classes相互关系”之间的差异。

故而:

  “public继承”意味着is-a。适用于base classes身上的每一件事情一定也适用于derived class身上,因为每一个derived class对象也都是一个base class对象。

条款33 : 避免遮掩继承而来的名称

  derived classes内的名称会遮掩base classes内的名称,即使base classes 和 derived classes 内的函数有不同的参数类型也适用,而且不论函数式virtual 或 non-virtual一体适用。在public继承下从来没有人希望如此。考虑如下代码:

class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
.....
};
class Derived : public Base
{
public:
virtual void mf1();
void mf3();
void mf4();
.....
};

//考虑下面调用
Derived d;
int x;
....
d.mf1();        // ok,调用Derived::mf1
d.mf1(x);      // error, 因为Derived::mf1遮掩了Base::mf1
d.mf2();        // ok, 调用Base::mf2
d.mf3();        // ok, 调用Derived::mf3
d.mf3(x);        //error, 因为Derived::mf3遮掩了Base::mf3


  Method1:如果你继承base class 并加上重载函数,而你又希望重新定义或覆写其中一部分,那么你必须为那些原本会被遮掩的每个名称引入一个using声明式,否则某些你希望继承的名称会被遮掩(using声明式会令继承而来的某给定名称之所有同名函数在derived class 中都可见。)。如下修改上述代码:

class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
.....
};
class Derived : public Base
{
public:
using Base::mf1;        //    让Base class内名为mf1 和mf3的所有东西
using Base::mf3;        // 在Derived 作用域内都可见
virtual void mf1();
void mf3();
void mf4();
.....
};

//考虑下面调用
Derived d;
int x;
....
d.mf1();        // ok,调用Derived::mf1
d.mf1(x);      // ok, 调用Base::mf1
d.mf2();        // ok, 调用Base::mf2
d.mf3();        // ok, 调用Derived::mf3
d.mf3(x);        //ok, 调用Base::mf3


  Method2:有时你并不想继承base classes的所有函数(当然,这在public继承下是绝对不允许的,因为它违反了public继承的“is-a”关系),这时可以采用一个简单的转交函数:

class Base
{
.....     // 与前同
};
class Derived : public Base
{
public:
virtual void mf1()          // 转交函数
{ Base::mf1(); }      // 暗自成为inline,条款30
};

//考虑下面调用
Derived d;
int x;
....
d.mf1();        // ok,调用Derived::mf1
d.mf1(x);      // error, Base::mf1() 被遮掩了


故而:

  1. derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。

  2. 为了让被遮掩的名称再见天日,可使用using声明式或转交函数。

条款34: 区分接口继承和实现继承

  1. 成员函数的接口总是会被继承。条款32所说,public继承意味is-a(是一种)。

  2. 声明一个pure virtual 函数的目的是为了让derived classes 只继承函数接口。令人意外的是,C++竟然允许我们为pure virtual函数提供定义,但调用它的唯一途径是“调用时明确指出其class名称”。(baseObj->BaseName::virtualFuncName();)

  3. 声明简朴的(非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。但是,允许impure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险。考虑如下代码:

// 现有A型、B型两种飞机,以相同方式飞行
class Airport
class Airplane
{
public:
virtual void fly(const Airport& destination);
....
};
void Airplane::fly(const Airport& destination)
{
// 缺省代码
}

class ModelA:public Airplane { ... };
class ModelB:public Airplane { ... };

//现有一C型飞机,以不同于A、B型的飞行方式飞行
class ModelC:public Airplane { ... };   //竟然忘记重新定义C型飞机的fly函数

// 如下调用会导致灾难
Airport pdx(...);
Airplane* pa = new ModelC;
...
pa->fly(pdx);     //


  上面代码的问题不在Airplane::fly有缺省行为,而在于ModelC在未明白说出“我要”的情况下就继承了该缺省行为(而此缺省行为却不是ModelC想要的)。我们必须做到“提供缺省实现给derived classes,但除非它们明白要求否则免谈”,解决方法在于切断“virtual函数接口”和其“缺省实现”之间的连接。可作如下修改:

// 现有A型、B型两种飞机,以相同方式飞行
class Airport
class Airplane
{
public:
virtual void fly(const Airport& destination) = 0; // 纯虚函数,迫使子类定义自己的实现
....
protected:
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Ariport& destination)
{
// 缺省实现
}

class ModelA:public Airplane
{
public:
virtual void fly(const Airport& destination)     //
{ defaultFly(destination); }
...
};
class ModelB:public Airplane
{
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
...
};

class ModelB:public Airplane
{
public:
virtual void fly(const Airport& destination);
...
};
void ModelC::fly(const Airport& destination)
{
// 实现C型飞机特有的飞行方式
}


  3. 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。

故而:

  1. 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。

  2. pure virtual函数只具体指定接口继承。

  3. 简朴的(非纯)impure virtual函数具体指定接口继承及缺省实现继承。

  4. non-virtual 函数具体指定接口继承以及强制性实现继承。

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

  假设我们有如下代码,其中有一个virtual函数:

class GameCharacter
{
public:
virtual int healthValue() const;  // 返回游戏角色血量
...
};


  Method1. 藉由NVI(Non-Virtual Interface)手法实现Template Method 模式(一种设计模式):

class GameCharacter
{
public:
int healthValue() const
{
...             // 做一些事前工作
int retVal = doHealthValue();
...             // 做一些事后工作
return retVal;
}
...
private:
virtual int doHealthValue() const  // derived classes可重新定义它
{
...
}
...
};


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

  NVI手法的一个优点隐式在上述代码注释“做一些事前工作”和“做一些事后工作”之中。这意味着外覆器确保得以在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清除场景。

  注意,NVI手法涉及在derived classes内重新定义private virtual函数。“重新定义virtual函数”表示某些事“如何”被完成,“调用virtual函数”则表示它“何时”被完成。NVI手法允许derived classes重新定义virtual函数,从而赋予它们“如何实现机能”的控制能力,但base class保留诉说“函数何时被调用”的权利。

  Method2(函数指针). 藉由Function Pointers实现Strategy模式:角色构造函数接受一个指针,指向一个健康计算函数(函数指针)。

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

int healthValue() const
{ return healthFunc(*this); }
...
private:
HealthCalcFunc healthFunc;
};


  这个做法是常见的Strategy设计模式的简单应用,它具有以下特点:

  (1)优点:同一个游戏角色之不同实体可以有不同的健康计算函数。

class EvilBagGuy : public GameCharacter
{
public:
explicit EvilBagGuy(HealthCalcFunc hcf = defaultHealthCalc)
: GameCharacter(hcf)
{ ... }
...
};
int loseHealthQuickly(const GameCharacter&);
int loseHealthSlowly(const GameCharacter&);
// 不同角色搭配不同计算方式
EvilBagGuy ebg1(loseHealthQuickly);
EvilBagGuy ebg2(loseHealthSlowly);


  (2)优点:游戏角色健康计算函数可在运行期变更。例如GameCharacter可提供一个成员函数setHealthCalcator,用来替换当前的健康计算函数。

  (3)缺点:角色的健康计算需要用到class 内的non-public信息的时候就会有问题(non-member 函数无权访问 non-public 成分)。唯一能够解决“需要以non-member函数访问class 的non-public成分”的办法就是:弱化class 封装。例如class可声明那个non-member函数为friends,或是为其实现的某一部分提供public访问函数(getter函数)。

  Method3(函数对象). 藉由tr1::function 完成Strategy模式:tr1::function对象可以包装任何可调用物(也就是函数指针、函数对象、或成员函数指针),只要其签名式兼容于需求端。具体可参见C++11新特性之八——函数对象function。可对Method2代码修改成使用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持有的是函数指针)比较,这个设计几乎相同。唯一不同的是如今GameCharacter持有一个tr1::function对象,相当于一个指向函数的泛化指针。这时,角色构造函数能接受的事物更具弹性:

short calcHealth(const GameCharacter&);      // 普通计算函数,返回non-int

struct HealthCalculator       // 使用clas类似
{
int operator() (const GameCharacter&) const      // 重写()操作符
{ ... }
};

class GameLevel
{
public:
float health(const GameCharacter&) const;      //成员函数,返回non-int
...
};

class EvilBagGuy : public GameCharacter
{
....
};

class EyeCandyCharacter : public GameCharacter
{
....
};

EvilBagGuy ebg1(calcHealth);    // 普通函数
EyeCandyCharacter ecc1(HealthCalculator());    //函数对象
GameLevel currentLevel;
...
EvilBagGuy ebg2(                // 成员函数
std::tr1::bind(&GameLevel::health,
currentLevel,
_1)
);


  这里需要对上述代码中的tr1::bind作下解析:首先表明,为计算ebg2的健康指数,应该使用GameLevel class 成员函数health。GameLevel::health 宣称它自己接受一个参数(那是个reference指向GameCharacter),但它实际上接受两个参数,因为它也获得一个隐式参数GameLevel,也就是this所指的那个。然而GameCharacter的健康计算函数只接受单一参数:GameCharacter(这个对象将被计算出健康指数)。如果我们使用GameLevel::health作为ebg2的健康计算函数,我们必须以某种方式转换它,使它不再接受两个参数(一个GameCharacter 和一个 GameLevel),转而接受单一参数(一个GameCharacter)。在这个例子中我们必然会想要使用currentLevel作为“ebg2的健康计算函数所需的那个GameLevel对象”,于是我们将currentLevel绑定为GameLevel对象,让它在“每次GameLevel::health被调用以计算ebg2的健康”时被使用。那正是tr1::bind的作为:它指出ebg2的健康计算函数应该总是以currentLevel作为GameLevel对象。tr1::bind具体见C++11新特性之二——std::bind std::function 高级用法

  Method4(接口类):古典的Strategy模式:本例中为,每一个GameCharacter对象都内含一个指针,指向一个来自HealthCalcFunc继承体系的对象。可画出UML设计图:

class GameCharacter;   // 前置声明
class HealthCalcFunc
{
public:
...
virtual int calc(const GameCharacter& gc) const
{ ... }
...
};
HealthCalcFunc defaultHealthCalc;

class GameCharacter
{
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc)
: pHealthFunc(phcf)
{}

int healthValue() const
{ return pHealthFunc->calc(*this); }
...
private:
HealthCalcFunc* pHealthFunc;
}


  这个做法典型的采用了Strategy模式。另外,它还提供“将一个既有的健康算法纳入使用”的可能性——只要为HealthCalcFunc继承体系添加一个derived class即可。

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

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

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

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

故而:

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

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

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

条款36: 绝不重新定义继承而来的non-virtual函数

  考虑下面代码:

class B
{
public:
void mf();
....
};
class D : public B
{
public:
void mf();     // 遮掩B::mf,条款33
.....
};

D x;    // D 对象
B* pB = &x;
D* pD = &x;
// 针对D对象x,下面语句竟然调用的是不同的mf
pB->mf();      // 调用B::mf
pD->mf();      // 调用D::mf


  造成此一两面行为的原因是,non-virtual函数如B::mf 和 D::mf都是静态绑定(条款37)。这意思是,由于pB被声明为一个pointer-to-B,通过 pB调用的 non-virtual 函数永远是B所定义的版本,即使pB指向一个类型为”B派生之class”的对象。更明确地说,当mf(base class 内为 non-virtual 并在derived class 内被重载的函数)被调用,如何一个D对象都可能表现出B或D的行为;决定因素不在对象自身,而在于“指向该对象之指针”当初的声明类型。References的行径类似。(virtual函数是动态绑定,所以不受这个问题之苦,条款07也解释过为什么动态性base classes内的析构函数应该是virtual.)。

故而:

  绝对不要重新定义继承而来的non-virtual函数。

条款37: 绝不重新定义继承而来的缺省参数值

  让我们一开始就将讨论简化。你只能继承两种函数:virtual和non-virtual函数。然而重新定义一个继承而来的non-virtual函数永远是错误的(条款36),所以我们可以安全地将本条款的讨论局限于“继承一个带有缺省参数值的virtual函数”。这种情况下,本条款成立的理由就非常直接而明确了:virtual函数系动态绑定,而缺省参数值却是静态绑定。

class Shape
{
public:
enum ShapeColor{ Red, Green, Blue };    // 提供类定义所必须的常量
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 继承缺省参数值。
// 但若以指针(或reference)调用此函数,可以不指定参数值。
// 因为动态绑定下这个函数会从其base 继承缺省参数值。
...
};
// 考虑下面指针
Shape* ps;                                        // 静态类型为Shape*, 没有动态类型, 尚未指向任何对象
Shape* pc = new Circle;                    // 静态类型为Shape*, 动态类型为Circle*
Shape* pr = new Rectangle;            // 静态类型为Shape*, 动态类型为Rectangle*

// 动态类型一如其名称所示,可在程序执行过程中改变(通常经由赋值完成)
ps = pc;        // ps 的动态类型如今是Circle*
ps = pr;            // ps 的动态类型如今是Rectangle*


  对象的所谓“静态类型”是指它在程序中被声明时所采用的类型(初始)。对象的所谓“动态类型”是指“目前所指对象的类型”(当前)。针对上面代码,考虑如下调用:

// 下面调用正常
pc->draw(Shape::Red);        // 调用Cricle::draw(Shape::Red)
pr->draw(Shape::Red);        // 调用Rectangle::draw(Shape::Red)
// 然而,下面调用则会出现问题
// 一如上面所说,virtual函数是动态绑定,而缺省参数值却是静态绑定
// 下面调用,你可能会在“调用一个定义于derived class内的virtual函数”的同时
// 却使用base class为它所指定的缺省参数值:
pr->draw();        // 调用Rectangle::draw(Shape::Red)


  此例子中,pr的动态类型是Rectangle*,所以调用的是Rectangle的virtual函数(动态绑定),一如你所预期。Rectangle::draw函数的缺省参数值应该是Green,但由于pr的静态类型是Shape*,所以此一调用的缺省参数值来自Shape class而非Rectangle class!(即使指针换成reference,问题依然存在)。

  C++为什么坚持以这种乖张的方式来运作呢?答案在于运行期效率。如果缺省参数值是动态绑定,编译器就必须有某种办法在运行期为virtual函数决定适当的参数缺省值。这比目前实行的“在编译器决定”的机制更慢而且更复杂。

  如果,客户想尝试遵守这条规则,并且同时提供缺省参数值给base 和 derived classes 的用户,如何:

class Shape
{
public:
enum ShapeColor{ Red, Green, Blue };    // 提供类定义所必须的常量
virtual void draw(ShapeColor color = Red) const = 0;
.....
};
class Rectangle : public Shape
{
public:
virtual void draw(ShapeColor color = Red) const;
...
};


  不幸的是,上面这样的代码导致代码重复。更糟的是,代码重复又带着相依性:如果Shape内的缺省参数值改变了,所有“重复给定缺省参数值”的那些derived classes也必须改变,否则它们最终会导致“重复定义一个继承而来的缺省参数值”。条款35列了不少virtual函数的替代设计,其中之一是NVI(non-virtual interface)手法:令base classes 内的一个public non-virtual 函数调用private virtual函数,后者可被derived classes重新定义。可作如下修改:

class Shape
{
public:
enum ShapeColor{ Red, Green, Blue };    // 提供类定义所必须的常量
void draw(ShapeColor color = Red) const    // non-virtual 函数
{
doDraw(color);     // 调用一个virtual
}
.....
private:
virtual void doDraw(ShapeColor color) const = 0;     // 真正的工作在此处完成
};
class Rectangle : public Shape
{
public:
...
private:
virtual void doDraw(ShapeColor color) const;  //注意,不须指定缺省参数值,但必须提供实现(可实现子类特定的颜色),因为base class 内为纯虚函数(pure virtual)
...
};


由于non-virtual 函数应该绝对不被derived classes覆写(条款36),这个设计很清楚地使得draw函数的color缺省参数值总是Red。
故而:

  绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定的,而virtual函数——你唯一应该覆写的东西——却是动态绑定。

条款38: 通过复合塑模出has-a或“根据某物实现出”

  复合(composition)是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。条款32曾说,“public继承”带有is-a(是一种)的意义。复合也有它自己的意义。实际上它有两个意义。复合意味has-a(有一个)或 is-implemented-in-terms-of(根据某物实现出)。那是因为你正打算在你的软件中处理两个不同领域(domains)。程序中的对象其实相当于你所塑造的世界中的某些事物,例如人、汽车等等。这样的对象属于应用域部分。其他对象则纯粹是实现细节上的人工制品,如缓冲区、互斥器等等。这些对象相当于你的软件的实现域。当复合发生于应用域内的对象之间,表现出has-a的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系。

  区分is-a和has-a并不困难,比较麻烦的是区分is-a(是一种)和 is-implemented-in-terms-of(根据某物实现出)这两种对象关系。  

  假如我们现在需要一组classes用来表现由不重复对象组成的sets。那么,考虑如下操作:
  1. 复用标准程序库提供的set template。不幸的是,标准程序库的set以平衡查找树实现,使它们在查找、安插、移除元素保证拥有对数时间效率,但空间销毁较大。而假设我们的程序,空间比效率更重要,那么标准程序库set template便不满足;

  2. 让你的set template继承std::list,毕竟实现sets的一种方法,便是在底层采用linked lists(链表)。而刚好,标准程序库有一个list template。然而, public继承中,一如条款32所说,对基类为真的每一件事,对派生类也应该为真。但list 可以内含重复元素,而我们的set,很不幸,不能内含重复元素。由于两个classes之间并非is-a关系,所有public继承不适合用来塑模它们。

  解决方案:Set对象可根据一个list对象实现出来。is-implemented-in-terms-of(根据某物实现出,是指使用了某物的特性,但又不完全相同,因为我们可能额外添加了一些特性,也可能在利用某物特性之前和之后做一些特殊操作,外覆在“某物特性”上面。)

template<class T>
class Set
{
public:
bool member(const T& item) const;
void insert(const T& item);
void remove(const T& item);
std::size_t size() const;
private:
std::list<T> rep;         // 用来表述set数据
};

// Set成员函数可大量倚赖list 及标准程序库其他部分提供的机能来完成
template<typename T>
bool Set<T>::member(const T& item) const
{
return std::find(rep.begin(), rep.end(), item) != rep.end();
}

template<typename T>
void Set<T>::insert(const T& item)
{
if(!member(item)) rep.push_back(item);     // 判重
}

template<typename T>
void Set<T>::remove(const T& item)
{
// typename 见条款42
typename std::list<T>::iterator it = std::find(rep.begin(), rep.end(), item);
if (it != rep.end()) rep.erase(it);
}

template<typename T>
std::size_t Set<T>::size(const T& item) const
{
return rep.size();
}


故而:
  1. 复合的意义和public继承完全不同。

  2. 在应用域,复合意味has-a(有一个)。在实现域,复合意味is-implemented-in-terms-of(根据某物实现出)。

条款39: 明智而审慎地使用private继承

  private继承意味is-implemented-in-terms-of(根据某物实现出)。如果你让class D以private形式继承class B,你的用意是为了采用class B 内已经备妥的某些特性,不是因为B对象和D对象存在任何观念上的关系。private继承纯粹只是一种实现技术(这就是为什么继承自一个private base class 的每样东西在你的class 内都是private:因为它们都只是实现枝节而已)。借用条款34 提出的术语,private 继承意味只有实现部分被继承,接口部分应略去。如果D 以private 形式继承 B,意思是 D 对象根据 B 对象实现而得,再没有其他意涵了。private 继承在软件“设计”层面上没有意义,其意义只及于软件“实现”层面。
  条款38 指出复合的意义也是is-implemented-in-terms-of(根据某物实现出)。对于这两者,尽可能使用复合,必要时才使用private继承。何时才必要呢?主要是当protected 成员(派生类能访问到父类的protected 成员,所以必须是在类的继承体系之下;而复合不属于类的继承体系下)和/或 virtual 函数(virtual 函数会导致动态绑定,这也需要在类的继承体系之下完成)。
  考虑这样一种情况:假设我们有个Widget class, 现在我们想在运行期间周期性的记录审查它的每个函数被调用次数,此刻,我们需要某种定时器,代码如下:

class Timer
{
public:
explicit Timer(int tickFrequency);
virtual void onTick() const;   // 定时器每滴答一次,此函数就被自动调用一次
....
};

// Method1:以private形式继承Timer
class Widget : private Timer
{
private:
virtual void onTick() const;
...
};

// Method2:public 继承 加 复合
class Widget
{
private:
class WidgetTimer : public Timer
{
public:
virtual void onTick() const;
...
};
WidgetTimer timer;
....
};


  这里,我们可能会选择Method2,原因:

  (1)你或许回想设计Widget 使它得以拥有derived classes,但同时你可能会想阻止derived classes重新定义onTick。如果Widget 继承自Timer,上面的想法就不可能实现,即使是private 继承也不可能(条款35 曾说过,derived classes 可以重新定义 virtual函数,即使它们不得调用(virtual 是私有)),但如果WidgetTimer 是 Widget 内部的一个private 成员并继承 Timer,Widget 的derived classes 将无法取用WidgetTimer(派生类 也无法访问 基类的 private 成员),因此无法继承它或重新定义它的virtual函数。

  (2)你或许会想将Widget 的编译依存性降至最低。如果Widget 继承Timer,当Widget 被编译时Timer 的定义必须可见,所以定义Widget 的那个文件恐怕必须#include Timer.h。但如果WidgetTimer 移出Widget之外而Widget内含指针指向一个WidgetTimer,Widget 可以只带着一个简单的WidgetTimer 声明式,不再需要#include 任何与Timer有关的东西,条款31。

  有一种特殊情况,可能会促使你选择“private 继承” 而不是 “继承加复合”:你所处理的class不带任何数据。这样的class (1)没有non-static成员变量,(2)没有virtual函数(因为这种函数的存在会为每个对象带来一个vptr,条款07),(3)也没有virtual base classes(因为这样的base classes 也会招致体积上的额外开销,条款40)。于是这种所谓的empty classes 对象不使用任何空间,因为没有任何隶属对象的数据需要存储。然而由于技术上的理由,C++裁定凡是独立(非附属)对象都必须有非零大小,所以,如果你这样做:

class Empty { };     // 没有数据,所以其对象应该不使用任何内存

class HoldsAnInt
{
private:
int x;
Empty e;
};


  你会发现sizeof(HoldsAnInt) > sizeof(int);一个Empty成员变量竟然要求内存!在大多数编译器中sizeof(Empty)获得1,因为面对“大小为零之独立(非附属)对象”,通常C++官方勒令默默安插一个char到空对象内。然而齐位需求(条款50)可能造成编译器为类似HoldsAnInt 这样的 class 加上一个衬垫,所以有可能HoldsAnInt 对象不只获得一个char 大小,也许实际上被放大到足够有存放一个int。

  但或许你注意到了,我很小心地说“独立(非附属)”对象的大小一定不为零。也就是说,这个约束不适用于derived class对象内的base class成分,因为它们并非独立(非附属)。如果你继承Empty,而不是内含一个那种类型的对象:

class Empty { };     // 没有数据,所以其对象应该不使用任何内存

class HoldsAnInt : private Empty
{
private:
int x;
};


  几乎可以确定sizeof(HoldsAnInt) > sizeof(int)。这是所谓的EBO(empty base optimization,空白基类最优化),我试过所有的编译器都有这样的结果。值得注意的是,EBO 一般只在单一继承(而非多重继承)下才可行。

  现实中的“empty”classes并不真的是empty。虽然它们从未拥有non-static成员变量,却往往内含typedefs,enums,static成员变量,或non-virtual函数。STL 就有许多技术用途的empty classes,其中内含有用的成员(通常是typedefs),包括base classes unary_function 和 binary_function ,这些是“用户自定义之函数对象”通常会继承的classes。EBO的广泛实践,使这样的继承很少增加derived classes的大小。

故而:

  1. Private 继承意味 is-implemented-in-terms-of(根据某物实现出)。它通常比复合的级别低。但是当derived class需要访问 protected base class 的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。即便如此,一个混合了public继承和复合的设计,也能达成你所要的行为。

  2. 和复合不同,private 继承可以造成empty base 最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。

条款40: 明智而审慎地使用多重继承

  (1) 首先,需要认清一个事实,当多重继承(multiple inheritance;MI)进入设计景框,程序有可能从一个以上的base classes 继承相同的名称(如函数,typedef等等),那会导致较多的歧义机会。

class BorrowableItem
{
public:
void checkOut();      //
....
};
class ElectronicGadget
{
private:
bool checkOut() const;
.....
};
class MP3Player :
public BorrowableItem,
public ElectronicGadget
{ .... };

MP3Player mp;
mp.checkOut();        // 歧义,调用的是哪个checkOut ?


  注意此例中对checkOut的调用是歧义(模棱两可)的,即使两个函数之中只有一个可取用(BorrowableItem内的checkOut是public, ElectronicGadget内的却是private)。这与C++用来解析重载函数调用的规则相符:在看到是否有个函数可取用之前,C++ 首先确认这个函数对此调用而言是最佳匹配。找出最佳匹配函数后才检查其可取用性(是否最佳匹配—>是否可取用)。本例的两个checkOuts有相同的匹配程度(因此才造成歧义),没有所谓最佳匹配。因此ElectronicGadget::checkOut的可取用性也就从未被编译器审查。

  为了解决这个歧义,你必须明白指出你要调用哪一个base class 内的函数:

mp.BorrowableItem::checkOut();    // 明白调用此函数
// 尝试调用ElectronicGadget::checkOut 将会获得一个“尝试调用private成员函数”的错误


  (2) 多重继承可能导致“钻石型多重继承”(菱形继承):

class File { ... };
class InputFile : public File { ... };
class OutputFile : public File { ... };
class IOFile : public InputFile, public OutputFile
{ ... };


  这样,File 中的成员变量将沿着InputFile和OutputFile两条路径被复制,最终在IOFile中造成两份成员变量。而C++缺省也是这么做的。如果你只需要在IOFile中保留一份File的成员变量,那么你必须令File 成为一个 virtual base class(虚基类,不同于抽象基类)。为了这样做,你必须令所有直接继承自它的classes 采用“virtual继承”(虚继承导致虚基类,保留基类的成员变量至多一份。):

class File { ... };
class InputFile : virtual public File { ... };
class OutputFile : virtual public File { ... };
class IOFile : public InputFile, public OutputFile
{ ... };


  virtual 继承所需要付出的成本是巨大的:首先,使用virtual 继承的那些classes 所产生的对象往往比使用non-virtual 继承的兄弟们的体积大,访问virtual base classes 的成员变量时,也比访问non-virtual base classes的成员变量速度慢。其次,virtual base 的初始化责任是由继承体系中的最低层 class 负责,这暗示:(1)classes 若派生自virtual bases而需要初始化,必须认知其virtual bases——不论那些bases 距离多远,(2)当一个新的derived class加入继承体系中,它必须承担其virtual bases(不论直接或间接)的初始化责任。

  所以,对于virtual base class(亦相当于使用non-virtual继承):第一,非必要不使用virtual bases。平常请使用non-virtual继承。第二,如果你必须使用virtual base class,尽可能避免在其中放置数据。Java 和 .NET 的Interfaces 便是不允许含有任何数据。

  (3)下面代码说明多重继承也有其合理用途,塑模“人”的C++ Interface class(条款31):

// 多重继承合理用途例子:
// 这个类指出需要实现的接口
class IPerson
{
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
};

class DataBaseID  { ... };

// 用以实现IPerson接口的类,将被private继承
class PersonInfo
{
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char* theName() const;
virtual const char* theBirthDate() const;
virtual const char* valueDelimOpen() const;
virtual const char* valueDelimClose() const;
....
};

// CPerson 必须实现IPerson接口,需得以public 继承才能完成
// CPerson 需要重新定义valueDelimOpen 和valueDelimClose,
// 单纯复合无法应付,这里我们采用private 继承。
// private 继承自 PersonInfo,导致PersonInfo中的virtual函数在CPerson成为Private成员,和在CPerson中定义Private实现细目类似
class CPerson : public IPerson, private PersonInfo  // 多重继承
{
public:
explicit CPerson(DatabaseID pid) : PersonInfo(pid) { }
virtual std::string name() const    // 实现必要的IPerson成员函数
{ return PersonInfo::theName(); }   // theName 内调用valueDelimOpen(Close)函数
virtual std::string birthDate() const
{ return PersonInfo::theBirthDate(); }
private:
const char* valueDelimOpen() const { return ""; }    // 重新定义继承而来的virtual函数
const char* valueDelimClose() const { return ""; }
  private:
     // std::string nameStr;   // 若有实现细目,置于具象类中并为private
};


  有了上面的interface class ,客户便可以使用factory function(工厂函数,条款31)将“派生自IPerson 的具象classes”实体化。

// factory function,根据一个独一无二的数据库ID 创建一个Person对象,条款18 告诉你为什么返回智能指针
// 也可以将createPerson声明在接口类IPerson中并使其成为static(接口类无法创建对象)。
std::tr1::shared_ptr<IPerson> createPerson(DatabaseID personIdentifier);
// 如下createPerson实现
std::tr1::shared_ptr<IPerson> createPerson(DatabaseID personIdentifier)
{
return
     // 由于CPerson public继承自 IPerson(为is-a关系)
std::tr1::shared_ptr<IPerson> (new CPerson(personIdentifier));
}
// 如下调用createPerson
DatabaseID askUserForDatabaseID();
DatabaseID id(askUserForDatabaseID());
...
std::tr1::shared_ptr<IPerson> pp(createPerson(id));


  注意:条款31中所描述的情况是,为了把class中的实现细目抽离出来,我们可以实现出handle class 或 interface class,以达到降低编译相依度的目的。本条款(3)中在此基础上还增加了对virtual函数的处理,为了同时将virtual函数抽离,我们private继承 PersonInfo。

故而:
  1. 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承(虚基类)的需要。

  2. virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base class 不带任何数据,将是最具实用价值的情况。

  3. 多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface class”和“private 继承某个协助实现的class”的两相组合。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: