Item 40:明智地使用多继承
多继承
多继承是 C++ 特有的概念,在是否应使用多继承的问题上始终争论不断。一派认为单继承是好的,所以多继承更好; 另一派认为多继承带来的麻烦更多,应该避免多继承。
- 多继承比单继承复杂,引入了歧义的问题,以及虚继承的必要性;
- 虚继承在大小、速度、初始化/赋值的复杂性上有不小的代价,当虚基类中没有数据时还是比较合适的;
- 多继承有时也是有用的。典型的场景便是:public 继承自一些接口类,private 继承自那些实现相关的类。
歧义的名称
多继承遇到的首要问题便是父类名称冲突时调用的歧义。如:
class A{
public:
void func();
};
class B{
private:
bool func() const;
};
class C: public A, public B{ ... };
C c;
c.func(); // 歧义
虽然 B::func 是私有的,但仍然会编译错。这是由C++的重载函数调用的解析规则决定的, 首先找到参数最匹配的函数,然后再检查可见性。上述例子中并未找到最匹配的函数,所以抛出了编译错误。 为了解决歧义,你必须这样调用:
c.A::func();
多继承菱形
当多继承的父类拥有更高的继承层级时,可能产生更复杂的问题比如多继承菱形(deadly MI diamond)。如图:
class File{};
class InputFile: public File{};
class OutputFile: public File{};
class IOFile: public InputFile, public OutputFile{};
这样的层级在 C++ 标准库中也存在,例如basic_ios, basic_istream, basic_ostream, basic_iostream。
IOFile 的两个父类都继承自 File,那么 File 的属性(比如filename)应该在 IOFile 中保存一份还是两份呢? 这是取决于应用场景的,就 File::filename 来讲显然我们希望它只保存一份,但在其他情形下可能需要保存两份数据。 C++ 还是一贯的采取了自己的风格:都支持!默认是保存两份数据的方式。如果你希望只存储一份,可以用 virtual 继承:
class File{};
class InputFile: virtual public File{};
class OutputFile: virtual public File{};
class IOFile: public InputFile, public OutputFile{};
可能多数情况下我们都是希望 virtual 的方式来继承。但总是用 virtual 也是不合适的,它有代价:
- 虚继承类的对象会更大一些;
- 虚继承类的成员访问会更慢一些;
- 虚继承类的初始化更反直觉一些。继承层级的最底层(most derived class)负责虚基类的初始化,而且负责整个继承链上所有虚基类的初始化。
基于这些复杂性,Scott Meyers 对于多继承的建议是:
- 如果能不使用多继承,就不用他;
- 如果一定要多继承,尽量不在里面放数据,也就避免了虚基类初始化的问题。
接口类
这样的一个不包含数据的虚基类和 Java 或者 C# 提供的 Interface 有很多共同之处,这样的类在 C++ 中称为接口类, 一个 Person 的接口类是这样的:
class IPerson {
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
};
由于客户无法创建抽象类的对象,所以必须以指针或引用的方式使用 IPerson。 需要创建实例时客户会调用一些工厂方法,比如:
shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);
同时继承接口类与实现类
在 Java 中一个典型的类会拥有这样的继承关系:
public class A extends B implements IC, ID{}
继承 B 通常意味着实现继承,继承 IC 和 ID 通常意味着接口继承。在 C++ 中没有接口的概念,但我们有接口类! 于是这时就可以多继承:
class CPerson: public IPerson, private PersonInfo{};
PersonInfo 是私有继承,因为 Person 是借助 PersonInfo 实现的。 对象组合是比 private 继承更好的实现继承方式。 但如果我们希望在 CPerson 中重写 PersonInfo 的虚函数,那么就只能使用上述的 private 继承了(这时就是一个合理的多继承场景)。
现在来设想一个需要重写虚函数的场景: 比如 PersonInfo 里面有一个 print 函数来输出 name, address,phone。但它们之间的分隔符被设计为可被子类定制的:
class PersonInfo{
public:
void print(){
char d = delimiter();
cout<<name<<d<<address<<d<<phone;
}
virtual char delimiter() const{ return ','; }
};
CPerson 通过 private 继承复用 PersonInfo 的实现后便可以重写 delimiter 函数了:
class CPerson: public IPerson, private PersonInfo{
public:
virtual char delimiter() const{ return ':'; }
...
};
总结
- 多继承单继承更复杂。它能导致新的歧义问题和对虚拟继承的需要。
- 虚拟继承增加了大小和速度成本,以及初始化和赋值的复杂度。当虚拟基类没有数据时它是最适用的。
- 多继承有合理的用途。一种方案涉及组合从一个接口类的公有继承和从一个有助于实现的类的私有继承。