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 "";}
};

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

posted @ 2012-02-14 15:53  lidan  阅读(516)  评论(0编辑  收藏  举报