条款40: 明智而审慎地使用多重继承
Use multiple inheritance judiciously一旦涉及多重继承(multiple inheritance:MI),C++社群便分为两个基本阵营.其中之一认为如果单一继承(single inheritance,SI)是好的,多重继承一定更好.另一派阵营则主张,单一继承是好的,但多重继承不值得拥有(或使用).本条款的主要目的是了解多重继承的两个观点.
最先需要认清的一件事是:当MI进入设计景框,程序有可能从一个以上的base class 继承相同名称,那么会导致较多的歧义的可能.例如:
class BorrowableItem {
public:
void checkOut();
...
};
class ElectronicGadget {
private:
bool checkOut() const;
...
};
class MP3Player : public BorrowableItem, public ElectronicGadget
{};
MP3Player mp;
mp.checkOut();
注意此例中对checkOut的调用是歧义的,即使两个函数中只有一个可取用,这与C++用来解析重载函数的规则相符:
在看到是否有个函数可取用之前,C++首先确认这个函数对此调用而言是最佳匹配.找到最佳匹配后才检验其可取用性.本例的两个checkOut有相同的匹配程度(因此造成歧义),没有所谓最佳匹配.因此,ElectronicGadget::checkOut的可取用性也就从未被编译器审查.
为了解决这个歧义,必须明白地指出要调用哪一个base class 内的函数:
mp.BorrowableItem::checkOut();
当然也可以尝试明确地调用ElectronicGadget::checkOut,但然后会获得一个"尝试调用private成员函数"的错误.
多重继承的意思是继承一个以上的base class,但这些 class 可能还有更高级的base class,那样就会导致要命的"钻石型多重继承":
class File { ... };
class InputFile : public File { ... };
class OutputFile : public File { ... };
class IOFile : public InputFile, public OutputFile { ... };
任何时候如果有一个继承而其中某个base class 和某个derived class 之间有一条以上的相通路线(就像上述的File和IOFile之间有两条路径)
,就必须面对这样一个问题:是否打算让base class 内的成员变量经由每一条路径被复制?假设File class 有个成员变量fileName,那么IOFile内有多少笔这个名称的数据呢?从某个角度说,IOFile从其每一个base class 继承一份,所以其对象内应该有两份fileName成员变量.但从另一个角度来说,IOFile对象只该有一个文件名称,所以它继承自两个base class 而来的fileName不该重复.
C++在这场辩论中并没有立场:两个方案它都支持——虽然其 缺省做法是执行复制(也就是第一种做法).如果想要 第二种,也就是只有一份数据的情况,那就必须令那个带有此数据的 class 成为一个 virtual base class.为了这样做,必须令所有直接继承自它的 class 采用"virtual继承":
class File { ... };
class InputFile : virtual public File { ... };
class OutputFile : virtual public File { ... };
class IOFile : public InputFile, public OutputFile { ... };
从正确行为的观点来看,public 继承应该总是 virtual.如果这是唯一一个观点,规则很简单:任何时候当使用 public 继承,请改用 virtual public 继承.但使用 virtual 继承的那些 class 所产生的对象往往比使用non-virtual 继承的 class 的体积大,访问 virtual base class 的成员变量时,也比访问non-virtual base class 的成员变量速度慢.种种细节因编译器不同而异,但基本重点很清楚:需要为 virtual 继承付出代价.
virtual 继承的成本还包括其他方面,支配"virtual base class初始化"的规则比起non-virtual base class 的情况复杂且不直观.virtual base的初始化责任是由继承体系中的最底层 class(most derived)负责,这暗示(1)class 若派生自 virtual base 而需要初始化,必须认知其 virtual base,不论那些base距离多远,(2)当一个新的derived class 加入继承体系中,它必须承担其 virtual base(不论直接或间接)的初始化责任.
关于 virtual base class(相当于对 virtual 继承)的忠告很简单.第一,非必要不使用 virtual base.平时请使用non-virtual 继承.第二, 如果必须使用 virtual base class,尽可能避免在其中放置数据.这样一来就不需要担心这些 class 身上的初始化(和赋值)所带来的诡异事情了.Java和.NET的Interface值得注意,它在许多方面兼容于C++的 virtual base class,而且也允许含有任何数据.
现在看看下面这个用来模塑"人"的C++ Interface class(详见条款31):
class IPerson {
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
};
IPerson的客户必须以IPerson的pointer和reference来编写程序,因为抽象 class 无法被实体化创建对象.为了创建一些可被当作IPerson来使用的对象,IPerson的客户使用factory function(工厂函数,详见
条款31)将"派生自IPerson的具象class"实体化:
// factory function(工厂函数),根据一个独一无二的数据库ID创建一个Person对象
// 条款18告诉为什么返回类型不是原始指针
std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);
// 这个函数从使用者手上取得一个数据库ID
DatabaseID id(askUserForDatabaseID());
std::tr1::shared_ptr<IPerson> pp(makePerson(id));
// 创建一个对象支持IPerson接口,借由IPerson成员函数处理*pp
但是makePerson如何创建对象并返回一个指针指向它呢?无疑地一定有某些派生自IPerson的具象 class,在其中makePerson可以创建对象.
假设这个 class 名为CPerson,就像具象 class 一样,CPerson必须提供"继承自IPerson"的pure virtual 函数的实现代码.
多重继承的一个合理的应用是:将"public继承自某接口"和"private继承自某实现"结合在一起:
class IPerson {
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
};
class DatabaseID { ... };
class PersonInfo {
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char* theName() const;
...
};
class CPerson : public IPerson, private PersonInfo { // 注意多重继承
public:
explicit CPerson(DatabaseID pid) : PersonInfo(pid) {}
virtual std::string name() const
{ return PersonInfo::theName(); }
...
private:
const char* valueDelimOpen() const { return ""; }
const char* valueDelimClose() const { return ""; }
};
这个例子指出,多重继承也有它的合理用途.
注意:
多重继承比单一继承复杂,它可能导致新的歧义性,以及对 virtual 继承的需要.
virtual 继承会增加大小,速度,初始化(以及赋值)复杂度等等成本.如果 virtual base class 不带任何数据,将是最具实用价值的情况.
多重继承的确有正当用途.其中一个情节涉及"public继承某个Interface class"和"private继承某个协助实现的class"的两相组合.