《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 "";}
};
如果你唯一能提出的设计涉及多重继承,你应该再努力想一想 —— 几乎可以说一定会有某些方案让单一继承行的通。然而有时候多继承的确是完成任务最简洁、最易维护、最合理的做法,就别害怕使用它。只是确定,的确在明智而审慎的情况下使用它。
请记住:
多重继承比单一继承复杂。它可能导致新的歧义性,以及对 virtual 继承的需求。
virtual 继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果 virtual base class 不带任何数据,将是最具实用价值的情况。
多重继承的确有正当用途。其中一个情节涉及 “public 继承某个 Interface class” 和 “private 继承某个协助实现的 class” 的两相组合。