六、继承与面向对象设计
Item32. 确定你的public继承塑模出is-a关系
(即Liskov Substitution Principle)
下面是一个不严谨的public继承:
class Bird { public: virtual void fly(); // birds can fly ... }; class Penguin:public Bird { // penguins are birds ... };
Penguin并不会飞。
所以一个较好的替代方案是:
class Bird { ... // no fly function is declared }; class FlyingBird: public Bird { public: virtual void fly(); ... }; class Penguin: public Bird { ... // no fly function is declared };
好的设计应该让违反这种关系的继承在编译期间就被检测出来。
public继承主张:能够施行于base class对象身上的_每件事情_都可以施行于derived class对象身上。
Item33. 避免遮掩继承而来的名称
C++会遮掩外层的函数名称,而不论它们的参数类型相同与否,也不管这些函数是否是虚函数。(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: using Base::mf1; // make all things in Base named mf1 and mf3 using Base::mf3; // visible (and public) in Derived's scope virtual void mf1(); void mf3(); void mf4(); ... };
使用using声明可以解决这个问题:
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; // make all things in Base named mf1 and mf3 using Base::mf3; // visible (and public) in Derived's scope virtual void mf1(); void mf3(); void mf4(); ... };
但是using声明式会使某一名称的函数在derived class中全部可见(如上例中,using Base::mf3会使其两个版本都可见)。这在public继承中是正确的(因为它是is-a关系),但在private继承中,可能会只想继承其中某一个版本(如那个无参数版本)。则需要下面的转交技术:
class Base { public: virtual void mf1() = 0; virtual void mf1(int); ... // as before }; class Derived: private Base { public: virtual void mf1() // forwarding function; implicitly { Base::mf1(); } // inline (see Item30) ... }; Derived d; int x; d.mf1(); // fine, calls Derived::mf1 d.mf1(x); // error! Base::mf1() is hidden
Item34. 区分接口继承和实现继承
在设计上可以让一个要被继承的纯虚函数(如fly)同时体现出两个基本要素:
其声明部分表现的是接口(那是derived classes必须使用的),其定义部分则表现出缺省行为(那是derived classes可能使用的,但只有在它们明确提出申请时才是):
class Airplane { public: virtual void fly(const Airport& destination) = 0; ... }; void Airplane::fly(const Airport& destination) // an implementation of { // a pure virtual function default code for flying an airplane to the given destination } class ModelA: public Airplane { public: virtual void fly(const Airport& destination) { Airplane::fly(destination); } ... }; class ModelB: public Airplane { public: virtual void fly(const Airport& destination) { Airplane::fly(destination); } ... }; class ModelC: public Airplane { public: virtual void fly(const Airport& destination); ... }; void ModelC::fly(const Airport& destination) { code for flying a ModelC airplane to the given destination }
小结:pure virtual函数只具体指定接口继承,impure virtual函数具体指定接口继承及缺省实现继承(这样的函数没有不变性,随derived classes而变),non-virtual函数具体指定接口继承以及强制性实现继承(这样的函数没有特化的空间,只能是base class中的那个)。
Item35. 考虑virtual函数以外的其他选择
1.籍由Non-Virtual Interface(NVI)实现Template Method模式
源自一个流派,他们主张virtual函数应该几乎总是private。
class GameCharacter { public: int healthValue() const // derived classes do not redefine { // this — see Item 36 ... // do "before" stuff — see below int retVal = doHealthValue(); // do the real work ... // do "after" stuff — see below return retVal; } ... private: virtual int doHealthValue() const // derived classes may redefine this { ... // default algorithm for calculating } // character's health };
即-令客户通过public non-virtual成员函数间接调用private virtual。
NVI的一个优点是do "before" stuff和do "after" stuff。这确保得以在一个virtual函数被调用之前设计好适当场景(locking a mutex、log extry、verifying that class invariants and function preconditions are satisfied, etc.),并在调用结束之后清理场景(unlocking a mutex、verifying function postconditions、reverifying class invariants, etc.)。
2.籍由Function Pointers实现Strategy模式
class GameCharacter; // forward declaration // function for the default health calculation algorithm 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; };
这样便得同一人物类型之不同实体可以有不同的健康计算函数。例如:
class EvilBadGuy: public GameCharacter { public: explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) : GameCharacter(hcf) { ... } ... }; int loseHealthQuickly(const GameCharacter&); // health calculation int loseHealthSlowly(const GameCharacter&); // funcs with different // behavior EvilBadGuy ebg1(loseHealthQuickly); // same-type charac- EvilBadGuy ebg2(loseHealthSlowly); // ters with different // health-related behavior
而且可以在运行期变更计算函数。但是其缺点是当这个传入的non-member函数需要访问class的non-public成分时,我们只能弱化class的封装(如设置为friend、提供public访问函数)。
3.籍由tr1::function完成Strategy模式
class GameCharacter; // as before int defaultHealthCalc(const GameCharacter& gc); // as before class GameCharacter { public: // HealthCalcFunc is any callable entity that can be called with // anything compatible with a GameCharacter and that returns anything // compatible with an int; see below for details typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc; explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {} int healthValue() const { return healthFunc(*this); } ... private: HealthCalcFunc healthFunc; };
签名式std::tr1::function<int (const GameCharacter&)>的含义是:接受一个reference指向const
GameCharacter,并返回int。tr1::function产生的对象可以持有任何与此签名式兼容的可调用物。
tr1::function比function pointer拥有更大的弹性,下面是三种情况:
①函数
short calcHealth(const GameCharacter&); // health calculation function;
//note non-int return type
②函数对象
struct HealthCalculator { // class for health
int operator()(const GameCharacter&) const // calculation function
{ ... } // objects
};
③成员函数
class GameLevel {
public:
float health(const GameCharacter&) const; // health calculation
... // mem function; note
}; // non-int return type
定义一些使用者
class EvilBadGuy: public GameCharacter { // as before
...
};
class EyeCandyCharacter: public GameCharacter { // another character
... // type; assume same
}; // constructor as EvilBadGuy
三种使用方法
EvilBadGuy ebg1(calcHealth); // character using a
// health calculation function
EyeCandyCharacter ecc1(HealthCalculator()); // character using a
// health calculation function object
GameLevel currentLevel;
...
EvilBadGuy ebg2( // character using a
std::tr1::bind(&GameLevel::health, // health calculation
currentLevel, // member function;
_1) // see below for details
);
(将currentLevel绑定为GameLevel对象,_1表示当为ebg2调用GameLevel::health时系以currentLevel作为GameLevel对象)
4.古典的Strategy模式
class GameCharacter; // forward declaration class HealthCalcFunc { public: ... virtual int calc(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; };
Item36. 绝不重新定义继承而来的non-virtual函数
如果A继承自B,又override了它的non-virtual函数,则此函数被调用时,可能表现出B或A的行为,这得看指
向该对象之指针的声明类型,因为non-virtual函数是statically bound。
Item37. 绝不重新定义继承而来的缺省参数值
因为缺省参数值都是静态绑定,而virtual函数却是动态绑定。违反了此条款,就导致调用一个定义于derived
class内的virtual函数的同时,却使用base class为它所指定的缺省参数值。
这时可以考虑替代设计,如NVI:
class Shape { public: enum ShapeColor { Red, Green, Blue }; void draw(ShapeColor color = Red) const // now non-virtual { doDraw(color); // calls a virtual } ... private: virtual void doDraw(ShapeColor color) const = 0; // the actual work is }; // done in this func class Rectangle: public Shape { public: ... private: virtual void doDraw(ShapeColor color) const; // note lack of a ... // default param val. };
Item38. 通过composition塑模出has-a或is-implemented-in-terms-of
比如你想通过在底层使用list来构造一个set:
template<typename T> // the wrong way to use list for Set
class Set: public std::list<T> { ... };
但这种public继承违反了is-a的含义,list中可以有重复元素,但set不可以有,因此set不是一个list。
正确做法是set object can be implemented in terms of a list object:
template<class T> // the right way to use list for Set 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; // representation for Set data };
Item39. 明智而审慎地使用private继承
由private继承而来的所有成员,在derived class中都会变成private属性。因此它们都成了实现的枝节。private继承意味着implemented-in-terms-of,这纯粹只是一种实现技术,在软件“设计”层面上没有意义,只在软件实现层面上有意义。
如果让class D以private继承class B,含义是为了采用B中已经备妥的某些特性,而不是因为B和D存在有任何观念上的关系。
Item38指出composition也用于实现implemented-in-terms-of,但要尽可能使用复合,必要时才使用private继承。
例如一个private继承:
class Widget: private Timer { private: virtual void onTick() const; // look at Widget usage data, etc. ... };
也可以换成composition的设计:
class Widget { private: class WidgetTimer: public Timer { public: virtual void onTick() const; ... }; WidgetTimer timer; ... };
composition的设计有两个优点:
1. 使得Widget可以拥有derived classes,但同时又可以阻止derived classes重新定义onTick。这一点private继承无法做到。(此法可用于模拟Java中的final或C#中的sealed)
2. 可以将Widget的编译依存性降至最低。即使WidgetTimer移出Widget体外,也只需要一个class
WidgetTimer声明式,而不用#include WidgetTimer,这是private继承所无法避免的。
Private继承有一个优点:可以实现EBO(empty base optimization)。
比较以下,先是composition:
class Empty {}; // has no data, so objects should use no memory class HoldsAnInt { // should need only space for an int private: int x; Empty e; // should require no memory };
将会发现sizeof(HoldsAnInt) > sizeof(int);
而在private中:
class HoldsAnInt: private Empty { private: int x; };
几乎可以肯定:sizeof(HoldsAnInt) == sizeof(int)
另外,当derived class需要访问protected base class的成员,或者需要重新定义继承而来的virtual函数时,使用private设计是合理的。
Item40. 明智而审慎地使用多重继承
为了避免钻石型继承,应该对直接继承自顶层的classes采用virtual继承:
class File { ... };
class InputFile: virtual public File { ... };
class OutputFile: virtual public File { ... };
class IOFile: public InputFile, public OutputFile { ... };
(thy:virtual继承专为多重继承引入,如果不是virtual继承的话那么如下:
class A{ public: void f(){}; }; class B: virtual public A{ public: void f(){}; }; class C: virtual public A{ public: void f(){}; }; class D: public C, public B{}; D d;
则d.f(); 调用的是B::f(); 如果C,B不是virtual public A的话那d.f()就必须指出f()是类C的还是类D的,d.B::f()或d.C::f())
但是virtual继承会比non-virtual继承体积大,访问成员速度慢。另外virtual base的初始化责任是由继承体系中的最低层(most derived)class负责,这意味着若classes派生自virtual bases而需要初始化,它必须认知virtual bases——不论距离多远。
所以 1.非必要不使用virtual bases,2.如果一定要用,尽可能避免在其中放置数据。
此外,在设计时可以将public继承与private继承加以结合:
class CPerson: public IPerson, private PersonInfo {}
使用“public继承接口”和“private继承实现”。