条款39:明智而审慎地使用private继承
private继承的特点:
- 如果类之间是private继承关系,那么编译器不会自动将一个派生类对象转换为一个基类对象。
- 由private继承来的所有成员,在派生类中都会变为private属性,换句话说,private继承只继承实现,不继承接口。
与类的复合关系相似,private继承正是表达“通过某工具类实现另一个类”。那么相似的,工具类在目标类中自然应该被隐藏—所有接口和变量都不应对外暴露出来。这也解释了private继承的内涵,它本质是一种技术封装,和public继承不同的是,private继承表达的是“只有实现部分被继承,而接口部分应略去”的思想。
与private继承的内涵相对应,在private继承下,父类的所有成员都转为子类私有变量——不提供对外访问的权限,外界也无需关心子类内有关父类的任何细节。
当我们拥有“用一个类去实现另一个类”的需求的时候,如何在类的复合与private继承中做选择呢?
- 尽可能用复合,除非必要,不要采用private继承。
- 当我们需要对工具类的某些方法(虚函数)做重载时,我们应选择private继承,这些方法一般都是工具类内专门为继承而设计的调用或回调接口,需要用户自行定制实现。
如果使用private继承,我们无法防止当前子类覆写后的虚函数被它的子类继续覆写,这种要求类似于对某个接口(函数)加上关键字final一样。为了实现对目标类的方法的防覆写保护,我们的做法是,在目标类中声明一私有嵌套类,该嵌套类public继承工具类,并在嵌套类的实现中覆写工具类的方法。
class TargetClass { //目标类
private:
class ToolHelperClass : public ToolClass { //嵌套类,public继承工具类
public:
void someMethod() override; //本应被目标类覆写的方法在嵌套类中实现,这样TargetClass的子类就无法覆写该方法。
}
}
如此一来,目标类的子类就无法再次覆写我们想要保护的核心方法。
绝大部分private继承的使用场合都可以被“public继承+复合”完美解决:
class Timer {
public:
explicit Timer(int tickFrequency);
virtual void OnTick() const;
...
};
class Widget : private Timer {
private:
virtual void OnTick() const;
...
};
替代为:
class Widget {
private:
class WidgetTimer : public Timer {
public:
virtual void OnTick() const;
...
};
WidgetTimer timer;
...
};
使用后者比前者好的原因有以下几点:
- private继承无法阻止其派生类重新定义虚函数,但若使用public继承定义
WidgetTimer
类并复合在Widget
类中,就能防止在Widget
类中重新定义虚函数。 - 可以仅提供
WidgetTimer
类的声明,并将WidgetTimer
类的具体定义移至实现文件中,从而降低Widget
的编译依存性。
然而private继承并非完全一无是处,一个适用于它的极端情况是空白基类最优化(empty base optimization,EBO),参考以下例子:
class Empty {};
class HoldsAnInt {
private:
int x;
Empty e;
};
一个没有非静态成员变量、虚函数的类,看似不需要任何存储空间,但实际上 C++ 规定凡是独立对象都必须有非零大小,因此此处sizeof(HoldsAnInt)
必然大于sizeof(int)
,通常会多出一字节大小,但有时考虑到内存对齐之类的要求,可能会多出更多的空间。
使用private继承可以避免产生额外存储空间(EBO一般只在单继承下可行),将上面的代码替代为:
class HoldsAnInt : private Empty {
private:
int x;
};
注:当派生类需要访问基类的protected的成员时或需要重新定义继承而来的virtual函数时,才被迫使用private继承,否则一般使用复合或public继承。
条款 40:明智而审慎地使用多重继承
多重继承是一个可能会造成很多歧义和误解的设计,因此反对它的声音此起彼伏,下面我们来接触几个使用多重继承的场景。
最先需要认清的一件事是,程序有可能从一个以上的基类继承相同名称,那会导致较多的歧义机会:
class BorrowableItem {
public:
void CheckOut();
...
};
class ElectronicGadget {
public:
void CheckOut() const;
...
};
class MP3Player : public BorrowableItem, public ElectronicGadget {
...
};
MP3Player mp;
mp.CheckOut(); // MP3Player::CheckOut 不明确!
如果真遇到这种情况,必须明确地指出要调用哪一个基类中的函数:
mp.BorrowableItem::CheckOut(); // 使用 BorrowableItem::CheckOut
在使用多重继承时,我们可能会遇到要命的“钻石型继承(菱形继承)”:
class File { ... };
class InputFile : public File { ... };
class OutputFile : public File { ... };
class IOFile : public InputFile, public OutputFile { ... };
这时候必须面对这样一个问题:是否打算让基类内的成员变量经由每一条路径被复制?如果不想要这样,应当使用虚继承,指出其愿意共享基类:
然而由于虚继承会在派生类中额外存储信息来确认成员来自于哪个基类,虚继承通常会付出更多空间和速度的代价,并且由于虚基类的初始化责任是由继承体系中最底层的派生类负责,就导致了派生类必须认知其虚基类并且承担虚基类的初始化责任。因此我们应当遵循以下两个建议:
- 非必要不使用虚继承。
- 如果必须使用虚继承,尽可能避免在虚基类中放置数据。
多重继承的确有正当用途,可用于结合public继承和private继承,public继承用于提供接口,private继承用于提供实现:
// IPerson 类指出要实现的接口
class IPerson {
public:
virtual ~IPerson();
virtual std::string Name() const = 0;
virtual std::string BirthDate() const = 0;
};
class DatabaseID { ... };
// PersonInfo 类有若干已实现的函数
// 可用以实现 IPerson 接口
class PersonInfo {
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char* TheName() const;
virtual const char* TheBirthDate() const;
virtual const char* ValueDelimOpen() const;
virtual const char* ValueDelimClose() const;
...
};
// CPerson 类使用多重继承
class CPerson: public IPerson, private PersonInfo {
public:
explicit CPerson(DatabaseID pid): PersonInfo(pid) {}
virtual std::string Name() const { // 实现必要的 IPerson 成员函数
return PersonInfo::TheName();
}
virtual std::string BirthDate() const { // 实现必要的 IPerson 成员函数
return PersonInfo::TheBirthDate();
}
private:
// 重新定义继承而来的虚函数
const char* ValueDelimOpen() const { return ""; }
const char* ValueDelimClose() const { return ""; }
};