条款 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 makePerson(DatabaseID personIdentifier); 

DatabaseID askUserForDatabaseID(); 

DatabaseID id(askUserForDatabaseID()); 

std::tr1::shared_ptr 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” 的两相组合。

你可能感兴趣的:(条款 40:明智而审慎地使用多重继承)