32:确定你的public继承塑模出is-a关系
以C++进行面向对象编程,最重要的一个规则是:public继承表示的是"is-a"(是一种)的关系。
如果令class D以public形式继承class B,你便是告诉编译器说,每一个类型为D的对象同时也是一个类型为B的对象,但是反之不成立。你主张“B对象可派上用场的任何地方,D对象一样可以派上用场”,因为每一个D对象都是一种B对象。
具体到代码上,任何函数如果期望获得一个类型为B(或pointer-to-B或reference-to-B)的实参,都也愿意接受一个D对象(或pointer-to-D或reference-to-D)。
这个论点只对pubiic继承才成立。private继承的意义与此完全不同(见条款39),至于protected继承,那是一种其意义至今仍然困惑我的东西。
public继承和is-a之间的等价关系听起来颇为简单,但有时候你的直觉可能会误导你。举个例子:class square应该以public形式继承class Rectangle吗?每个人都知道正方形是一种矩形,反之则不一定,这是真理,但是看下面的代码:
class Rectangle { public: virtual void setHeight(int newHeight); virtual void setWidth(int newWidth); virtual int height() const; // return current values virtual int width() const; }; void makeBigger(Rectangle& r) // function to increase r's area { int oldHeight = r.height(); r.setWidth(r.width() + 10); // add 10 to r's width assert(r.height() == oldHeight); // assert that r's }
显然,上述的assert结果永远为真。因为makeBigger只改变r的宽度,r的高度从未被更改。
现在考虑这段代码,其中使用public继承,允许正方形被视为一种矩形:
class Square: public Rectangle {...}; Square s; assert(s.width() == s.height()); makeBigger(s); assert(s.width() == s.height());
很明显,第二个assert结果也应该永远为真。因为根据定义,正方形的宽度和其高度相同。但现在我们遇上了一个问题。我们如何调解下面各个assert判断式:调用makeBigger之前,在makeBigger函数内s的高度和宽度相同;s的宽度改变,但高度不变;makeBigger返回之后,s的高度再度和其宽度相同。
本例的根本困难是,某些可施行于矩形身上的事情却不可施行于正方形身上。但是public继承主张,能够施行于base class对象身上的每件事情,也可以施行于derived class对象身上。在正方形和矩形例子中,那样的主张无法保持,所以以public继承塑模它们之间的关系并不正确。
33:避免遮掩继承而来的名称
1:下面的代码是一个很简单的名称遮掩的例子:
int x; void someFunc() { double x; std::cin >> x; }
someFunc的x是double类型而global x是int类型,但那不要紧。C++的名称遮掩规则所做的唯一事情就是:遮掩名称。至于名称是否对应相同的类型,并不重要。本例中一个名为x的double遮掩了一个名为x的int。
2:导入继承之后,当派生类成员函数内引用(refer to)基类内的某物(成员函数、typedef、或成员变量)时,编译器可以找出我们所refer to的东西,因为派生类继承了声明于基类内的所有东西。实际运作方式是,派生类作用域被嵌套在基类作用域内,像这样:
class Base { private: int x; public: virtual void mf1() = 0; virtual void mf2(); void mf3(); }; class Derived: public Base { public: virtual void mf1(); void mf4(); }; void Derived::mf4() { mf2(); }
此例内含一组混合了public和private名称,以及一组成员变量和成员函数名称。这些成员函数包括pure virtual,impure virtual和non-virtual三种,这是为了强调我们谈的是名称,和其他无关。这个例子也可以加入各种名称类型,例如~,nested classes和typedef。整个讨论中唯一重要的是这些东西的名称,至于这些东西是什么并不重要。
在Derived::mf4函数中,当编译器看到这里使用名称mf2,必须估算它refer to什么东西。编译器首先查找local作用域(也就是mf4覆盖的作用域),在那儿没找到任何东西名为mf2。于是查找其外围作用域,也就是class Derived覆盖的作用域。还是没找到任何东西名为mf2,于是再往外围移动,本例为base class。在那儿编译器找到一个名为mf2的东西了,于是停止查找。如果Base内还是没有mf2,查找动作便继续下去,首先找内含Base的那个namespace(s)的作用域(如果有的话),最后往global作用域找去。
再次考虑上面的例子,这次让我们重载base中的mf1和mf3,并且添加一个新版mf3到Derived去:
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(); };
现在,base class内所有名为mf1和mf3的函数都被derived class内的mf1和mf3函数遮掩掉了。从名称查找观点来看,Base::mf1和Base::mf3不再被Derived继承!
Derived d; int x; d.mf1(); // fine, calls Derived::mf1 d.mf1(x); // error! Derived::mf1 hides Base::mf1 d.mf2(); // fine, calls Base::mf2 d.mf3(); // fine, calls Derived::mf3 d.mf3(x); // error! Derived::mf3 hides Base::mf3
如你所见,即使base classes和derived classes内的函数有不同的参数类型,而且不论函数是virtual或non-virtual,都会发生名称遮蔽。这和本条款一开始展示的道理相同,如今Derived内的函数mf3遮掩了一个名为mf3但类型不同的base函数。
不幸的是你通常会想继承重载函数。实际上如果你正在使用public继承而又不继承那些重载函数,就就违反了base和derived classes之间的is-a关系。可以使用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(); };
现在,继承机制将一如往昔地运作:
Derived d; int x; d.mf1(); // still fine, still calls Derived::mf1 d.mf1(x); // now okay, calls Base::mf1 d.mf2(); // still fine, still calls Base::mf2 d.mf3(); // fine, calls Derived::mf3 d.mf3(x); // now okay, calls Base::mf3
有时候你并不想继承base classes的所有函数,这是可以理解的。但是在public继承下,这绝对不可能发生,因为它违反了public继承所暗示的“base和derived classes之间的is-a关系”。这也就是为什么上述using声明式被放在derived class的public区域的原因:base class内的public名称在publicly derived class内也应该是public。
然而在private继承之下它却可能是有意义的。假设Derived以private形式继承Base,而Derived唯一想继承的mf1是那个无参数版本。using声明式在这里派不上用场,因为using声明式会令继承而来的某给定名称之所有同名函数在derived class中都可见。我们需要不同的技术,即一个简单的forwarding函数:
class Base { public: virtual void mf1() = 0; virtual void mf1(int); ... // as before }; class Derived: private Base { public: virtual void mf1() // forwarding function { Base::mf1(); } ... }; Derived d; int x; d.mf1(); // fine, calls Derived::mf1 d.mf1(x); // error! Base::mf1() is hidden
34:区分接口继承和实现继承
表面上直截了当的public继承概念,经过更严密的检查之后,发现它由两部分组成:函数接口继承和函数实现继承。身为class设计者,有时候你会希望derived classes只继承成员函数的接口(也就是声明);有时候你又会希望derived classes同时继承函数的接口和默认实现,但又希望它能够覆写(override)它们所继承的实现;有时候你希望derived classes同时继承函数的接口和实现,并且不允许覆写任何东西。
考虑下面的代码:
class Shape { public: virtual void draw() const = 0; virtual void error(const std::string& msg); int objectID() const; }; class Rectangle: public Shape { ... }; class Ellipse: public Shape { ... };
Shape类中声明了三个函数:第一个是纯虚函数draw,它使得Shape成为了一个抽象类,所以客户不能够创建Shape类的实体,只能创建其derived classes的实体,而且derived classes中必须实现自己的draw函数(否则会报编译错误);第二个是虚函数error;第三个是普通函数objectID;
1:声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。
Shape::draw函数是个纯虚函数,因为所有Shape对象都应该是可绘出的,但Shape class无法为此函数提供合理的缺省实现,毕竟椭圆形绘法迥异于矩形绘法。Shape::draw的声明乃是对具象derived classes设计者说,“你必须提供一个draw函数,但我不干涉你怎么实现它。”
在C++中,可以为pure virtual函数提供定义。也就是说你可以为Shape::draw供应一份实现代码。
2:声明(非纯)虚函数的目的,是让derived classes继承该函数的接口和缺省实现。
Shape::error函数是个虚函数,它表示每个class都必须支持一个“当遇上错误时可调用”的函数,但每个class可自由处理错误。如果某个class不想针对错误做出任何特殊行为,它可以退回到Shape class提供的缺省错误处理行为。也就是说Shape::error的声明式告诉derived classes的设计者,“你必须支持一个error函数,但如果你不想自己写一个,可以使用Shape class提供的缺省版本”。
3:声明普通非虚函数的目的是为了令derived classes继承函数的接口及一份强制实现。
如果成员函数是个非虚函数,意味是它并不打算在derived classes中有不同的行为。实际上一个非虚成员函数所表现的不变性(invariant)凌驾其特异性(specialization ),因为它表示不论derived class变得多么特异化,它的行为都不可以改变,所以它绝不该在derived class中被重新定义。
Shape::objectID函数是个非虚函数,它的声明表示:“每个Shape对象都有一个用来产生对象识别码的函数;此识别码总是采用相同计算方法,该方法由Shape::objectID的定义式决定,任何derived class都不应该尝试改变其行为”。
35:考虑virtual函数以外的选择
下面的代码中,GameCharacter表示游戏中的人物角色,成员函数healthValue表示人物的健康程度:
class GameCharacter { public: virtual int healthValue() const; ... };
由于不同的人物可能以不同的方式计算他们的健康指数,因此将healthValue声明为virtual似乎是再明白不过的做法,该函数并未被声明为pure virtual,这暗示我们将会有个计算健康指数的缺省算法。
下面是几种不使用virtual的替代方法:
1:由Non-Virtual interface手法实现Template Method模式
该方法主张virtual函数应该几乎总是private。这个流派的拥护者建议,较好的设计是保留healthVaiue为public成员函数,但让它成为non-virtual,并调用一个private virtual函数进行实际工作:
class GameCharacter { public: int healthValue() const { printf("begin of healthValue\n"); int retVal = doHealthValue(); printf("end of healthValue\n"); return retVal; } private: virtual int doHealthValue() const { printf("this is GameCharacter::doHealthValue\n"); } }; class GCA : public GameCharacter { private: virtual int doHealthValue() const { printf("this is GCA::doHealthValue\n"); } }; int main() { GameCharacter gc; GCA gca; gc.healthValue(); gca.healthValue(); GameCharacter *pgc1 = new GameCharacter; GameCharacter *pgc2 = new GCA; pgc1->healthValue(); pgc2->healthValue(); delete pgc1; delete pgc2; }
这种方法,也就是“令客户通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface(NVI)手法。它是所谓Template Method设计模式(与C++ templates并无关联)的一个独特表现形式。
NVI手法的一个优点隐身在“做一些事前工作”和“做一些事后工作”之中。也就是确保得以在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清理场景。上述代码的结果如下:
begin of healthValue this is GameCharacter::doHealthValue end of healthValue begin of healthValue this is GCA::doHealthValue end of healthValue begin of healthValue this is GameCharacter::doHealthValue end of healthValue begin of healthValue this is GCA::doHealthValue end of healthValue
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; };
这个做法是常见的Strategy设计模式的简单应用。使用这种方法,同一人物类型之不同实体可以有不同的健康计算函数,而且某已知人物之健康指数计算函数可在运行期变更。例如GameCharacter可提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。
3:由std::function完成Strategy模式
基于函数指针的做法有些苛刻而死板:为什么要求“健康指数之计算”必须是个函数,而不能是某种“像函数的东西”呢?如果一定得是函数,为什么不能够是个成员函数?为什么一定得返回int而不是任何可被转换为int的类型呢?
可以改用一个类型为std::function的对象,这些约束就全都不见了。这样的对象可持有任何可调用物,比如函数指针、函数对象、或成员函数指针等:
class GameCharacter; int defaultHealthCalc(const GameCharacter& gc); 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::function<int (const GameCharacter&)> HealthCalcFunc; explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {} int healthValue() const { return healthFunc(*this); } private: HealthCalcFunc healthFunc; };
HealthCalcFunc是一种std::function类型,这种类型的对象可以持有任何与其签名函数兼容的可调用物。其签名函数“接受一个reference指向const GameCharacter,并返回int"。所谓兼容,意思是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可被隐式转换为int:
short calcHealth(const GameCharacter&); //返回short而非int struct HealthCalculator { //函数对象 int operator()(const GameCharacter&) const { ... } }; class GameLevel { public: float health(const GameCharacter&) const; //成员函数,返回float ... }; class EvilBadGuy: public GameCharacter { ... }; class EyeCandyCharacter: public GameCharacter { ... }; EvilBadGuy ebg1(calcHealth); //使用函数 EyeCandyCharacter ecc1(HealthCalculator()); //使用函数对象 GameLevel currentLevel; //使用成员函数 EvilBadGuy ebg2( std::bind(&GameLevel::health, currentLevel, _1) );
4:传统的Strategy模式
传统的Strategy做法会将健康计算函数做成一个分离的继承体系中的virtual成员函数。设计结果看起来像这样:
这张图表示GameCharacter是某个继承体系的根类,体系中的EvilBadGuy和EyeCandyCharacter都是derived classes;HealthCalcFunc是另一个继承体系的根类,体系中的S1owHealthLoser和FastHealthLoser都是derived classes,每一个GameCharacter对象都内含一个指针,指向一个来自HealthCalcF}nc继承体系的对象。具体的代码如下:
class GameCharacter; 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; };
36:绝不重新定义继承而来的non-virtual函数
之前的条款说过,所谓public继承意味is-a的关系;在class内声明一个non-virtual函数会为该class建立起一个不变性,凌驾其特异性。如果将这两个观点施行于两个classes:B(ase)和D(erived)以及non-virtual成员函数B::mf身上,意味着:适用于B对象的每一件事,也适用于D对象;B的derived classes一定会继承mf的接口和实现,因为mf是B的一个non-virtual函数。
如果D重新定义mf,这样便出现矛盾:如果D真有必要实现出与B不同的mf,那么“每个D都是一个B”就不为真。既然如此D就不该以public形式继承B。另一方面,如果D真的必须以public方式继承B,并且如果D真有需要实现出与B不同的mf,那么mf就无法为B反映出“不变性凌驾特异性”的性质。既然这样mf应该声明为virtual函数。最后,如果每个D真的是一个B,并且如果mf真的为B反映出“不变性凌驾特异性”的性质,那么D便不需要重新定义mf,而且它也不应该尝试这样做。
因此:任何情况下都不该重新定义一个继承而来的non-virtual函数。
37:绝不重新定义继承而来的缺省参数值
virtual函数系动态绑定,而缺省参数值却是静态绑定。
为什么C++坚持以这种乖张的方式来运作呢?答案在于运行期效率。如果缺省参数值是动态绑定,编译器就必须有某种办法在运行期为virtual函数决定适当的参数缺省值。这比目前实行的“在编译期决定”的机制更慢而且更复杂。为了程序的执行速度和编译器实现上的简易度,C++做了这样的取舍。
38:通过复合塑模出has-a或“根据某物实现出”(is-implemented-in-terms-of)
复合(composition)是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。例如:
class Address { ... }; class PhoneNumber { ... }; class Person { public: ... private: std::string name; // composed object Address address; // ditto PhoneNumber voiceNumber; // ditto PhoneNumber faxNumber; // ditto };
上面的代码中,Person对象由string,Address,PhoneNumber构成。
1:复合有两个意义:has-a(有一个),或这是is-implemented-in-terms-of(根据某物实现出)。
因为你正打算在你的软件中处理两个不同的领域。如果程序中的对象相当于世界中的某些事物,例如人、汽车、一张张视频画面等等。这样的对象属于应用域部分。其他对象则纯粹是实现细节上的人工制品,像是缓冲区、互斥锁、查找树等等。这些对象相当于软件的实现域。
当复合发生于应用域内的对象之间,表现出has-a的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系。
2:has-a的关系很好区分,比较麻烦的是区分is-a和is-implemented-in-terms-of这两种对象关系。
比如:某些情况下必须自己实现一个sets而不能使用标准库提供的版本。实现sets的方法很多,其中一种便是在底层采用标准库的linked lists。
首先想到让set
template// the wrong way to use list for Set class Set: public std::list { ... };
这是错误的,因为public继承意味着is-a的关系,如果D是一种B,对B为真的每一件事情对D也都应该为真。但list可以内含重复元素,但是set的定义却不允许包含重复元素。因此“Set是一种list”并不为真。
正确的做法是,Set对象可根据一个list对象实现出来:
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); ... private: std::listrep; // representation for Set data }; template bool Set ::member(const T& item) const { return std::find(rep.begin(), rep.end(), item) != rep.end(); } template void Set ::insert(const T& item) { if (!member(item)) rep.push_back(item); }
39:明智而审慎地使用private继承
1:如果classes之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象:
class Person { ... }; class Student: private Person { ... }; // inheritance is now private void eat(const Person& p); // anyone can eat Person p; // p is a Person Student s; // s is a Student eat(s); // error! a Student isn't a Person
上面针对s的eat调用将会报错,当eat的形参是Person或Person*时也一样,都会报错:error: ‘Person’ is an inaccessible base of ‘Student’。
由private base class继承而来的所有成员,在derived class中都会变成private属性,纵使它们在base class中原本是protected或public属性。
2:Private继承意味着implemented-in-terms-of(根据某物实现出)。如果让class D以private形式继承class B,你的用意是为了采用class B内已经具备的某些特性,不是因为B对象和D对象存在有任何观念上的关系。因此,private继承纯粹只是一种实现技术,如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其他意涵了。
3:Private继承意味is-implemented-in-terms-of,之前的条款指出复合的意义也是这样。如何在两者之间取舍?答案很简单:尽可能使用复合,必要时才使用private继承。何时才是必要?主要是当protected成员和/或virtual函数牵扯进来的时候。
4:为了能够知道Widget成员函数的调用频率,需要记录每个成员函数的调用次数,然后周期性的审查这些信息。为了完整这个工作,需要设定一个定时器,周期性的取出Widget的状态。
假设当前有一个定时器类:
class Timer { public: virtual void onTick() const; // automatically called for each tick ... };
onTick函数会周期性的执行。因此,可以重新定义那个onTick函数,让其取出Widget当前状态。
为了让Widget重新定义Timer内的virtual函数,Widget必须继承自Timer。但public继承在此例并不适当,因为Widget显然并不是个Timer。这种情况下,必须以private形式继承Timer:
class Widget: private Timer { private: virtual void onTick() const; // 查看Widget的数据等等.. ... };
藉由private继承,Timer的public OnTick在Widget内变成private了。
5:上面的方法不是唯一实现目的的方法,其实可以使用复合:
class Widget { private: class WidgetTimer: public Timer { public: virtual void onTick() const; ... }; WidgetTimer timer; };
使用复合要比使用private继承有更多的优势:首先,你或许会想设计Widget使它得以拥有derived classes,但同时你可能会想阻止derived classes重新定义onTick。如果Widget继承自Timer,上面的想法就不可能实现,即使是private继承也不可能。但如果WidgetTimer是Widget内部的一个private成员并继承Timer,Widget的derived classes将无法取用WidgetTimer,因此无法继承它或重新定义它的virtual函数。
6:private继承主要用于“当一个意欲成为derived class者想访问base class的protected成分,或为了重新定义一或多个virtual函数”,但这时候两个classes之间的概念关系其实是is-implemented-in-terms-of而非is-a。
当你面对并不存在is-a关系的两个classes,其中一个需要访问另一个的protected成员,或需要重新定义其一或多个virtual函数,private继承极有可能成为正统设计策略。
7:private继承还适用于一种比较激进的情况:如果一个类不带任何数据,也就是它没有non-static成员变量,没有virtual函数(因为这种函数会为每个对象带来一个vptr),也没有virtual base classes(这样的base classes也会导致体积的额外开销)。这种类的对象不使用任何空间,因为没有隶属于对象的数据需要存储。但是C++规定,凡是独立(非附属)的对象必须有非0大小,所以:
class Empty { public: fun() {printf("this is fun");} private: fun2() {printf("this is fun2");} }; class HoldsAnInt { private: int x; Empty e; };
上面的类定义,sizeof(HoldsAnInt)会大于sizeof(int),测试结果是sizeof(Empty)为1, sizeof(int)为4,而sizeof(HoldsAnInt)为8。因为面对大小为0的独立非附属对象,C++要求默默插入一个char到空对象中,然而因为内存对其的需求,所以sizeof(HoldsAnInt)为8。
上面的情况适用于独立非附属对象,但是不适用于derived class内的base class成分,因为它不是独立非附属的,因此:
class HoldsAnInt: private Empty { private: int x; };
这样的定义,sizeof(HoldsAnInt)等于sizeof(int),这就是所谓的EBO(empty base optimization;空白基类最优化),EBO一般只在单一继承可行。
注意,上面的空类不是真的empty,它可以包含typedefs, enums, static成员变量或non-virtual函数。
40:明智而审慎地使用多重继承
1:多重继承情况下,派生类可能从多个base class继承相同的名称,从而导致歧义:
class BorrowableItem { public: void checkOut(); }; class ElectronicGadget { private: bool checkOut(int a) const; }; class MP3Player: public BorrowableItem, public ElectronicGadget { }; MP3Player mp; mp.checkOut(); // ambiguous! which checkOut?
上面的代码对checkOut的调用会报错:” reference to ‘checkOut’ is ambiguous”,及时两个候选函数的访问权限不同,参数也不相同。为了解决歧义,必须明确指出要调用哪一个base class内的函数:mp.BorrowableItem::checkOut()
2:多重继承的情况下,有可能形成“钻石型多重继承”的情况。为了避免某个数据发生多份拷贝的情况,必须使那些带有此数据的class成为一个virtual base class:
class File { ... }; class InputFile: virtual public File { ... }; class OutputFile: virtual public File { ... }; class IOFile: public InputFile, public OutputFile { ... };
这种方法的缺点是:使用virtual继承的那些classes所产生的对象往往比使用non-virtual继承的兄弟们体积大,访问virtual base classes的成员变量时,也比访问non-virtual base classes的成员变量速度慢。种种细节因编译器不同而异,但基本重点很清楚:你得为virtual继承付出代价。
另外,virtual base的初始化责任是由继承体系中的最低层(most derived) class负责,这表示:(1)classes若派生自virtual bases而需要初始化,必须认知其virtual bases--不论那些bases距离多远;(2)当一个新的derived class加入继承体系中,它必须承担其virtual bases(不论直接或间接)的初始化责任。
我对virtual base classes(亦相当于对virtual继承)的忠告很简单。第一,非必要不使用virtual bases。平常请使用non-virtual继承。第二,如果你必须使用virtual base classes,尽可能避免在其中放置数据。这么一来你就不需担心这些classes身上的初始化(和赋值)所带来的诡异事情了。
下面是一种多重继承的合理应用场景:
class IPerson { public: virtual ~IPerson(); virtual std::string name() const = 0; virtual std::string birthDate() const = 0; };
IPerson是个Interface class,CPerson是要继承该类并需要提供继承自IPerson的pure virtual函数的实现代码。现在有个现成的类PersonInfo,它可以完成CPerson所需要的实际工作:
class PersonInfo { public: explicit PersonInfo(DatabaseID pid); virtual ~PersonInfo(); virtual const char * theName() const; virtual const char * theBirthDate() const; private: virtual const char * valueDelimOpen() const; // see virtual const char * valueDelimClose() const; // below };
PersonInfo用于以各种格式打印数据库字段,每个字段值的起始字符和终止字符由valueDelimOpen和valueDelimClose返回,默认的实现分别是’[’ 和 ’]’,但是这两个界限符号并非人人喜欢,因此valueDelimOpen和valueDelimClose是virtual函数,允许派生类设置自己的界限符号。所以,PersonInfo::theName的实现可能如下:
const char * PersonInfo::valueDelimOpen() const { return "["; // default opening delimiter } const char * PersonInfo::valueDelimClose() const { return "]"; // default closing delimiter } const char * PersonInfo::theName() const { static char value[Max_Formatted_Field_Value_Length]; // write opening delimiter std::strcpy(value, valueDelimOpen()); //append to the string in value this object's name field (being careful //to avoid buffer overruns!) // write closing delimiter std::strcat(value, valueDelimClose()); return value; }
作为CPerson的实现者,发现可以使用PersonInfo实现name和birthDate,但是需要界限符号为空。因此,CPerson和PersonInfo的关系是is-implemented-in-terms-of,我们知道这种关系可以有两种技术实现:复合和private继承。条款39指出复合通常是较受欢迎的做法,但如果需要重新定义virtual函数,那么继承是必要的。本例之中CPerson需要重新定义valueDelimOpen和valueDelimClose,所以单纯的复合无法应付。最直接的解法就是令CPerson以private形式继承PersonInfo。
CPerson也必须实现IPerson接口,因此需要以public继承IPerson。因此这就是多重继承的一个通情达理的应用:
class CPerson: public IPerson, private PersonInfo { // note use of MI public: explicit CPerson( DatabaseID pid): PersonInfo(pid) {} virtual std::string name() const { return PersonInfo::theName(); } virtual std::string birthDate() const { return PersonInfo::theBirthDate(); } private: const char * valueDelimOpen() const { return ""; } const char * valueDelimClose() const { return ""; } };
最后,需要注意的是,如果某种需求下,你唯一能够提出的设计方案涉及多重继承,你应该更努力想一想--几乎可以说一定会有某些方案让单一继承行得通。然而多重继承有时候的确是完成任务之最简洁、最易维护、最合理的做法,果真如此就别害怕使用它。只要确定,你的确是在明智而审慎的情况下使用它。