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

Effective C++ 中文版 第三版》读书笔记

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

一旦涉及多重继承 (multiple inheritance;MI):

程序有可能从一个以上的 base class 继承相同名称(如函数、typedef 等)。那会导致较多的歧义机会。例如:

class BorrowableItem { 
public: 
    void checkOut(); 
};

class ElectronicGadet { 
private: 
    bool checkOut() const; 
};

class MP3Player: public BorrowableItem 

public ElectronicGadet 
{...}; 

MP3Player mp; 

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

即使两个之中只有一个可取用(ElectronicGadet 是 private)。这与 C++ 用来解析重载函数调用的规则相符:在看到是否有个函数可取之前,C++ 首先确认这个函数对此调用之言是最佳匹配。找出最佳匹配才检验其可取用性。本例的两个 checkOut 有相同的匹配程度。没有所谓最佳匹配。因此 ElectronicGadget::checkOut 的可取用性就从未被编译器审查。

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

mp.BorrowableItem::checkOut();

你当然也可以明确调用 ElectronicGadget::checkOut(),但然后你会获得一个 “尝试调用 private 成员函数” 的错误。

当即称一个以上的 base classes,这些 base classes 并不常在继承体系中有更高级的 base classes,因为那会导致要命的 “钻石型多重继承”:

class File{...}; 

class InputFile: public File {...}; 

class OutputFile: public File{...}; 

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

任何时候只要你的继承体系中某个 base class 和某个 derived class 之间有一条以上的想通路线,你就必须面对这样一个问题:是否打算让 base class 内的成员经由每一条路径被复制?假设 File 有个成员变量 fileName,那么 IOFile 应给有两份 fileName 成员变量。但从另一个角度来说,简单的逻辑告诉我们,IOFile 对象只有一个文件名称,所以他继承自两个 base class 而来的 fileName 不能重复。

C++ 的缺省做法是执行重复。如果那不是你要的,你必须令那个带有此数据的 base class(也就是 File)成为一个 virtual base class。必须令所有直接继承自它的 classes 采用 “virtual 继承”:

class File{...}; 

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

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

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

C++ 标准程序库内含一个多重继承体系,只不过其 class 是 class template: basic_ios,basic_istream,basic_ostream 和 basic_iostream。

从正确行为来看,public 继承应该总是 virtual。如果这是唯一一个观点,规则很简单:任何时候当你使用 public 继承,请改用 virtual public 继承。但是,正确性并不是唯一观点。为避免继承来的成员变量重复,编译器必须提供若干幕后戏法,其后果就是:使用 virtual 继承的那些 classes 所产生的对象往往比使用 non-virtual 继承的兄弟们体积大,访问 virtual base classes 的成员变量时,也比访问 non-virtual base classes 成员变量速度慢。

virtual 继承的成本还包括其他:支配 “virtual base classes 初始化” 的规则比起 non-virtual base 的情况远为复杂和不直观。virtual base 的初始化责任是由继承体系中的最底层(most derived)class 负责,1、class 若派生自 virtual base class 而需要初始化,必须认知其 virtual bases —— 不论那些 bases 距离多远,2、当一个新的 derived class 加入继承体系中,它必须承担起 virtual bases(不论直接或间接)的初始化工作。

我们对 virtual 继承的忠告:第一,非必要不要使用 virtual bases。第二,如果必须使用 virtual bases,尽可能避免在其中放置数据。这样你就不需担心这些 classes 身上的初始化(和赋值)所带来的诡异事情了。

下面看看这个 C++ Interface class:

class IPerson{ 
public: 
    virtual ~IPerson(); 
    virtual std::string name() const = 0; 
    virtual std::string birthDate() const =0; 
};

//factory function,根据一个独一无二的数据库ID创建一个Person对象 
std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier); 

DatabaseID askUserForDatabaseID(); 

DatabaseID id(askUserForDatabaseID()); 

std::tr1::shared_ptr<IPerson> pp(makePerson(id));

假设一个派生自 IPerson 的具象 class CPerson,它必须提供 “继承自 Iperson” 的 pure virtual 函数的实现代码。我们可以写出这些,但更好的是利用既有组件。例如有个既有的数据库相关 class,PersonInfo:

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; 
    virtual const char* valueDelimClose() const; 
};

PersonInfo 被设计用来协助以各种格式打印数据库字段,每个字段值的起始点和结束点以特殊字符串为界。默认为 “[”,“]”,但并非人人都爱方括号,所以提供两个 virtual 函数 valueDelimOpen 和 ValueDelimClose 语序 derived class 设定他们自己的头尾界限符号。PersonInfo 成员函数将调用这些 virtual 函数,把适当的界限符号添加到它们的返回值上。PersonInfo::theName 的代码看起来像这样:

const char* PersonInfo::valueDelimOpen() const 
{ 
    return "[";//default 
} 

const char* PersonInfo::valueDelimClose() const 
{ 
    return "]";//default 
} 

const char* PersonInfo::theName() const 
{ 
    //保留缓冲区给返回值使用:static,自动初始化为“全0” 
    static char value[Max_Formatted_Field_Value_Length]; 

    //写入起始符号 
    std::strcpy(value, valueDelimOpen()); 

    //将value内的字符串附到这个对象的name成员变量中 

    //写入结尾符号 
    std::strcat(value, valueDelimClose()); 
    return value; 
}

所以 theName 返回的结果不仅仅取决于 PersonInfo 也取决于从 PersonInfo 派生下去的 classes。

Cperson 和 personInfo 的关系是,PersonInfo 刚好有若干函数可帮助 Cperson 比较容易实现出来。因此它们的关系是 is-implemented-in-term-of。这种关系可以两种技术实现:复合和 private 继承。一般复合必要受欢迎,本例之中 Cperson 要重新定义 valueDelimOpen 和 valueDelimClose,所以直接的解法是 private 继承。

Cperson 还有必须实现 Iperson 的接口,那得要 public 继承才能完成。这导致多重继承的一个通情达理的应用:将 “public 继承自某接口” 和 “private 继承自某实现” 结合在一起:

class Cperson: public IPerson, private PersonInfo{ 
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 "";} 
};

如果你唯一能提出的设计涉及多重继承,你应该再努力想一想 —— 几乎可以说一定会有某些方案让单一继承行的通。然而有时候多继承的确是完成任务最简洁、最易维护、最合理的做法,就别害怕使用它。只是确定,的确在明智而审慎的情况下使用它。

请记住:

  1. 多重继承比单一继承复杂。它可能导致新的歧义性,以及对 virtual 继承的需求。

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

  3. 多重继承的确有正当用途。其中一个情节涉及 “public 继承某个 Interface class” 和 “private 继承某个协助实现的 class” 的两相组合。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容

  • 《Effective C++ 中文版 第三版》读书笔记 ** 条款 39:明智而审慎地使用 private 继承 ...
    赵者也阅读 351评论 0 0
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,504评论 18 399
  • 再读高效c++,颇有收获,现将高效c++中的经典分享如下,希望对你有所帮助。 1、尽量以const \enum\i...
    橙小汁阅读 1,192评论 0 1
  • 这本书属于“想提高必看之书”,相见恨晚,建议所有C++程序员都看看,没事也可以拿出来翻翻。大家也可以浏览下面的笔记...
    拉普拉斯妖kk阅读 707评论 0 1