分享
 
 
 

C++箴言:谨慎使用多继承

王朝c/c++·作者佚名  2006-01-06
窄屏简体版  字體: |||超大  

触及 multiple inheritance (MI)(多继续)的时候,C++ 社区就会鲜明地分裂为两个基本的阵营。一个阵营认为假如 single inheritance (SI)(单继续)是有好处的,multiple inheritance(多继续)一定更有好处。另一个阵营认为 single inheritance(单继续)有好处,但是多继续引起的麻烦使它得不偿失。在本文中,我们的主要目的是理解在 MI 问题上的这两种看法。

首要的事情之一是要承认当将 MI 引入设计领域时,就有可能从多于一个的 base class(基类)中继续相同的名字(例如,函数,typedef,等等)。这就为歧义性提供了新的时机。例如:

class BorrowableItem { // something a library lets you borrow

public:

void checkOut(); // check the item out from the library

..

};

class ElectronicGadget {

PRivate:

bool checkOut() const; // perform self-test, return whether

... // test sUCceeds

};

class mp3Player: // note MI here

public BorrowableItem, // (some libraries loan MP3 players)

public ElectronicGadget

{ ... }; // class definition is unimportant

MP3Player mp;

mp.checkOut(); // ambiguous! which checkOut?

注重这个例子,即使两个函数中只有一个是可访问的,对 checkOut 的调用也是有歧义的。(checkOut 在 BorrowableItem 中是 public(公有)的,但在 ElectronicGadget 中是 private(私有)的。)这与 C++ 解析 overloaded functions(重载函数)调用的规则是一致的:在看到一个函数的是否可访问之前,C++ 首先确定与调用匹配最好的那个函数。只有在确定了 best-match function(最佳匹配函数)之后,才检查可访问性。这目前的情况下,两个 checkOuts 具有相同的匹配程度,所以就不存在最佳匹配。因此永远也不会检查到 ElectronicGadget::checkOut 的可访问性。

为了消除歧义性,你必须指定哪一个 base class(基类)的函数被调用:

mp.BorrowableItem::checkOut(); // ah, that checkOut...

当然,你也可以尝试显式调用 ElectronicGadget::checkOut,但这样做会有一个 "you're trying to call a private member function"(你试图调用一个私有成员函数)错误代替歧义性错误。

multiple inheritance(多继续)仅仅意味着从多于一个的 base class(基类)继续,但是在还有 higher-level base classes(更高层次基类)的 hierarchies(继续体系)中出现 MI 也并不罕见。这会导致有时被称为 "deadly MI diamond"(致命的多继续菱形)的后果。

class File { ... };

class InputFile: public File { ... };

class OutputFile: public File { ... };

class IOFile: public InputFile,

public OutputFile

{ ... };

在一个“在一个 base class(基类)和一个 derived class(派生类)之间有多于一条路径的 inheritance hierarchy(继续体系)”(就像上面在 File 和 IOFile 之间,有通过 InputFile 和 OutputFile 的两条路径)的任何时候,你都必须面对是否需要为每一条路径复制 base class(基类)中的 data members(数据成员)的问题。例如,假设 File class 有一个 data members(数据成员)fileName。IOFile 中应该有这个 field(字段)的多少个拷贝呢?一方面,它从它的每一个 base classes(基类)继续一个拷贝,这就暗示 IOFile 应该有两个 fileName data members(数据成员)。另一方面,简单的逻辑告诉我们一个 IOFile object(对象)应该仅有一个 file name(文件名),所以通过它的两个 base classes(基类)继续来的 fileName field(字段)不应该被复制。

C++ 在这个争议上没有自己的立场。它恰当地支持两种选项,虽然它的缺省方式是执行复制。假如那不是你想要的,你必须让这个 class(类)带有一个 virtual base class(虚拟基类)的数据(也就是 File)。为了做到这一点,你要让从它直接继续的所有的 classes(类)使用 virtual inheritance(虚拟继续):

class File { ... };

class InputFile: virtual public File { ... };

class OutputFile: virtual public File { ... };

class IOFile: public InputFile,

public OutputFile

{ ... };

标准 C++ 库包含一个和此类似的 MI hierarchy(继续体系),只是那个 classes(类)是 class templates(类模板),名字是 basic_ios,basic_istream,basic_ostream 和 basic_iostream,而不是 File,InputFile,OutputFile 和 IOFile。

从正确行为的观点看,public inheritance(公有继续)应该总是 virtual(虚拟)的。假如这是唯一的观点,规则就变得简单了:你使用 public inheritance(公有继续)的任何时候,都使用 virtual public inheritance(虚拟公有继续)。唉,正确性不是唯一的视角。避免 inherited fields(继续来的字段)复制需要在编译器的一部分做一些 behind-the-scenes legerdemain(幕后的戏法),而结果是从使用 virtual inheritance(虚拟继续)的 classes(类)创建的 objects(对象)通常比不使用 virtual inheritance(虚拟继续)的要大。访问 virtual base classes(虚拟基类)中的 data members(数据成员)也比那些 non-virtual base classes(非虚拟基类)中的要慢。编译器与编译器之间有一些细节不同,但基本的要点很清楚:virtual inheritance costs(虚拟继续要付出成本)。

它也有一些其它方面的成本。支配 initialization of virtual base classes(虚拟基类初始化)的规则比 non-virtual bases(非虚拟基类)的更加复杂而且更不直观。初始化一个 virtual base(虚拟基)的职责由 hierarchy(继续体系)中 most derived class(层次最低的派生类)承担。这个规则中包括的含义:

(1) 从需要 initialization(初始化)的 virtual bases(虚拟基)派生的 classes(类)必须知道它们的 virtual bases(虚拟基),无论它距离那个 bases(基)有多远;

(2) 当一个新的 derived class(派生类)被加入继续体系时,它必须为它的 virtual bases(虚拟基)(包括直接的和间接的)承担 initialization responsibilities(初始化职责)。

我对于 virtual base classes(虚拟基类)(也就是 virtual inheritance(虚拟继续))的建议很简单。首先,除非必需,否则不要使用 virtual bases(虚拟基)。缺省情况下,使用 non-virtual inheritance(非虚拟继续)。第二,假如你必须使用 virtual base classes(虚拟基类),试着避免在其中放置数据。这样你就不必在意它的 initialization(初始化)(以及它的 turns out(清空),assignment(赋值))规则中的一些怪癖。值得一提的是 java 和 .NET 中的 Interfaces(接口)不答应包含任何数据,它们在很多方面可以和 C++ 中的 virtual base classes(虚拟基类)相比照。

现在我们使用下面的 C++ Interface class(接口类)(参见《C++箴言:最小化文件之间的编译依靠》)来为 persons(人)建模:

class IPerson {

public:

virtual ~IPerson();

virtual std::string name() const = 0;

virtual std::string birthDate() const = 0;

};

IPerson 的客户只能使用 IPerson 的 pointers(指针)和 references(引用)进行编程,因为 abstract classes(抽象类)不能被实例化。为了创建能被当作 IPerson objects(对象)使用的 objects(对象),IPerson 的客户使用 factory functions(工厂函数)(再次参见 Item 31)instantiate(实例化)从 IPerson 派生的 concrete classes(具体类):

// factory function to create a Person object from a unique database ID;

// see Item 18 for why the return type isn't a raw pointer

std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);

// function to get a database ID from the user

DatabaseID askUserForDatabaseID();

DatabaseID id(askUserForDatabaseID());

std::tr1::shared_ptr<IPerson> pp(makePerson(id)); // create an object

// supporting the

// IPerson interface

... // manipulate *pp via

// IPerson's member

// functions

但是 makePerson 怎样创建它返回的 pointers(指针)所指向的 objects(对象)呢?显然,必须有一些 makePerson 可以实例化的从 IPerson 派生的 concrete class(具体类)。

假设这个 class(类)叫做 CPerson。作为一个 concrete class(具体类),CPerson 必须提供它从 IPerson 继续来的 pure virtual functions(纯虚拟函数)的 implementations(实现)。它可以从头开始写,但利用包含大多数或全部必需品的现有组件更好一些。例如,假设一个老式的 database-specific class(老式的数据库专用类)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

...

};

你可以看出这是一个老式的 class(类),因为 member functions(成员函数)返回 const char*s 而不是 string objects(对象)。尽管如此,假如鞋子合适,为什么不穿呢?这个 class(类)的 member functions(成员函数)的名字暗示结果很可能会非常合适。

你忽然发现 PersonInfo 是设计用来帮助以不同的格式打印 database fields(数据库字段)的,每一个字段的值的开始和结尾通过指定的字符串定界。缺省情况下,字段值开始和结尾定界符是方括号,所以字段值 "Ring-tailed Lemur" 很可能被安排成这种格式:

[Ring-tailed Lemur]

根据方括号并非满足 PersonInfo 的全体客户的期望的事实,virtual functions(虚拟函数)valueDelimOpen 和 valueDelimClose 答应 derived classes(派生类)指定它们自己的开始和结尾定界字符串。PersonInfo 的 member functions(成员函数)的 implementations(实现)调用这些 virtual functions(虚拟函数)在它们返回的值上加上适当的定界符。作为一个例子使用 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

{

// reserve buffer for return value; because this is

// static, it's automatically initialized to all zeros

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;

}

有人可能会质疑 PersonInfo::theName 的陈旧的设计(非凡是一个 fixed-size static buffer(固定大小静态缓冲区)的使用,这样的东西发生 overrun(越界)和 threading(线程)问题是比较普遍的——参见《C++箴言:必须返回对象时别返回引用》),但是请把这样的问题放到一边而注重这里:theName 调用 valueDelimOpen 生成它要返回的 string(字符串)的开始定界符,然后它生成名字值本身,然后它调用 valueDelimClose。

因为 valueDelimOpen 和 valueDelimClose 是 virtual functions(虚拟函数),theName 返回的结果不仅依靠于 PersonInfo,也依靠于从 PersonInfo 派生的 classes(类)。

对于 CPerson 的实现者,这是好消息,因为当细读 IPerson documentation(文档)中的 fine print(晦涩的条文)时,你发现 name 和 birthDate 需要返回未经修饰的值,也就是,不答应有定界符。换句话说,假如一个人的名字叫 Homer,对那个人的 name 函数的一次调用应该返回 "Homer",而不是 "[Homer]"。

CPerson 和 PersonInfo 之间的关系是 PersonInfo 碰巧有一些函数使得 CPerson 更轻易实现。这就是全部。因而它们的关系就是 is-implemented-in-terms-of,而我们知道有两种方法可以表现这一点:经由 composition(复合)(参见《C++箴言:通过composition模拟“has-a”》)和经由 private inheritance(私有继续)(参见《C++箴言:谨慎使用私有继续》)。《C++箴言:谨慎使用私有继续》指出 composition(复合)是通常的首选方法,但假如 virtual functions(虚拟函数)要被重定义,inheritance(继续)就是必不可少的。在当前情况下,CPerson 需要重定义 valueDelimOpen 和 valueDelimClose,所以简单的 composition(复合)做不到。最直截了当的解决方案是让 CPerson 从 PersonInfo privately inherit(私有继续),虽然 《C++箴言:谨慎使用私有继续》说过只要多做一点工作,则 CPerson 也能用 composition(复合)和 inheritance(继续)的组合有效地重定义 PersonInfo 的 virtuals(虚拟函数)。这里,我们用 private inheritance(私有继续)。

但是 CPerson 还必须实现 IPerson interface(接口),而这被称为 public inheritance(公有继续)。这就引出一个 multiple inheritance(多继续)的合理应用:组合 public inheritance of an interface(一个接口的公有继续)和 private inheritance of an implementation(一个实现的私有继续):

class IPerson { // this class specifies the

public: // interface to be implemented

virtual ~IPerson();

virtual std::string name() const = 0;

virtual std::string birthDate() const = 0;

};

class DatabaseID { ... }; // used below; details are

// unimportant

class PersonInfo { // this class has functions

public: // useful in implementing

explicit PersonInfo(DatabaseID pid); // the IPerson interface

virtual ~PersonInfo();

virtual const char * theName() const;

virtual const char * theBirthDate() const;

virtual const char * valueDelimOpen() const;

virtual const char * valueDelimClose() const;

...

};

class CPerson: public IPerson, private PersonInfo { // note use of MI

public:

explicit CPerson( DatabaseID pid): PersonInfo(pid) {}

virtual std::string name() const // implementations

{ return PersonInfo::theName(); } // of the required

// IPerson member

virtual std::string birthDate() const // functions

{ return PersonInfo::theBirthDate(); }

private: // redefinitions of

const char * valueDelimOpen() const { return ""; } // inherited virtual

const char * valueDelimClose() const { return ""; } // delimiter

}; // functions

在 UML 中,这个设计看起来像这样:

这个例子证实 MI 既是有用的,也是可理解的。

时至今日,multiple inheritance(多继续)不过是 object-oriented toolbox(面向对象工具箱)里的又一种工具而已,典型情况下,它的使用和理解更加复杂,所以假如你得到一个或多或少等同于一个 MI 设计的 SI 设计,则 SI 设计总是更加可取。假如你能拿出来的仅有的设计包含 MI,你应该更加专心地考虑一下——总会有一些方法使得 SI 也能做到。但同时,MI 有时是最清楚的,最易于维护的,最合理的完成工作的方法。在这种情况下,毫不畏惧地使用它。只是要确保谨慎地使用它。

Things to Remember

·multiple inheritance(多继续)比 single inheritance(单继续)更复杂。它能导致新的歧义问题和对 virtual inheritance(虚拟继续)的需要。

·virtual inheritance(虚拟继续)增加了 size(大小)和 speed(速度)成本,以及 initialization(初始化)和 assignment(赋值)的复杂度。当 virtual base classes(虚拟基类)没有数据时它是最适用的。

·multiple inheritance(多继续)有合理的用途。一种方案涉及组合从一个 Interface class(接口类)的 public inheritance(公有继续)和从一个有助于实现的 class(类)的 private inheritance(私有继续)。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有