Effective C++条款39:明智而审慎地使用private继承(Use private inheritance judiciously)
- 条款39:明智而审慎地使用private继承
- 1、private 继承
- 2、在private继承和复合之间做出正确选择
- 3、使用private继承比组合更加合理的例子
- 4、牢记
- 总结
《Effective C++》是一本轻薄短小的高密度的“专家经验积累”。本系列就是对Effective C++进行通读:
第6章:继承与面向对象设计
条款39:明智而审慎地使用private继承
1、private 继承
条款32论证过C++如何把public继承视为”is-a”关系来。考虑一个继承体系,其中类Student public 继承自类Person,于是编译器为了让函数成功调用需要将Student隐式转换为Person,这时候“is-a”关系就出现了。现在重复一部分该例,并以private继承替换public继承:
class Person { ... };
class Student: private Person { ... }; // 改用private继承
void eat(const Person& p); // 任何人都可以吃
void study(const Student& s); // 只有学生才在校学习
Person p; // p是人
Student s; // s 是学生
eat(p); // 没问题,p是人
eat(s); // error!
显然,private继承并不意味着“is-a”关系。那它意味着什么呢?
我们观察一下private继承的行为。private继承的规则:与public继承相反,如果类之间的继承关系是private,编译器不会将派生类对象(Student)转换成为基类对象(Person)。这和public继承的情况不同。这也是为什么为对象s调用eat会失败。第二条规则是,由private基类继承而来的所有成员,在派生类中都会变成private属性,即使在基类中的成员是protected或者public的,从此基类中private继承而来的成员会变成派生类中的private成员。
private继承意味着“is-implemented-in-terms-of”。如果你让类D private继承自类B,你的用意是因为你想利用类B中的一些让你感兴趣的性质,而不是因为在类型B和类型D之前有任何概念上的关系。private继承纯粹只是一种实现技术。(这也是为什么你从private基类中继承而来的任何东西在你的类中都变为了private的:所有的都只是实现上的细节。)借用条款34中引入的术语,private继承意味着只有实现部分被继承;而接口应该被忽略掉。如果类D private继承自类B,就意味着D对象的实现依赖于类B对象,没有别的含义了。private继承在软件实现层名才有意义,在软件设计层面是没有意义的。
2、在private继承和复合之间做出正确选择
private继承意味着“is-implemented-in-terms-of”的事实会让你感觉有一些不安,因为条款38中指出复合(composition)也同样意味着“is-implemented-in-terms-of”。
你应该怎么在它们之间做出取舍?答案是简单的:尽量使用复合(composition),在必须使用private继承的时候才去使用它。何时是必须使用?主要是当protected成员或者(和)虚函数被牵扯进来的时候,还有一种情况是,因为空间原因而不能使用private继承。
演示案例①:以public方式继承(错误做法)
假设我们正在一个涉及到Widgets类的应用上工作,我们决定应该较好的了解如何使用Widgets。例如,我们不只想知道Widget成员函数的调用有多频繁,也想知道经过一段时间后调用比例如何变化。
我们决定修改Widget类,让它记录每个成员函数的调用次数。在运行时,我们周期性地来审查这项信息,为了达到这个目的,我们会创建一个定时器于是我们可以知道什么时候去收集这些统计信息。
我们更乐意去重用代码,尽量少写新代码,例如下面这个类:
class Timer {
public:
explicit Timer(int tickFrequency);
virtual void onTick() const; // 定时器每滴答一次
... //此函数就自动调用一次
};
这就是我们要找的。我们可以为这个Timer对象配置任意的tick频率,在每个tick发生的时候,它会调用一个虚函数。我们可以重定义这个虚函数来检查Widget世界的当前状态。完美!
为了让Widget重定义Timer内的虚函数,Widget必须继承自Timer。但public继承是不合适的。因为Widget不是一个Timer。Widget客户不应该在一个Widget对象上调用onTick,因为onTick不是Widget的接口。并且允许这样的函数调用会使得客户很容易出现对Widget接口的误用,这很明显的违反了条款18的忠告:使接口容易被正确使用不容易被误用。Public继承在这里不是有效选择。
演示案例②:使用private继承
所以我们在这里使用private继承:
class Widget: private Timer {
private:
virtual void onTick() const;
...
}
借由private继承的力量,Timer的public onTick函数在Widget中变为了private,我们将其放在private关键字下并对其进行了重新声明。
演示案例③:以复合的形式实现
这是个很好的设计,因为private继承不是绝对必须的,我们决定使用组合(compostion)来替代private继承,是可以的。只要在Widget内部声明一个内嵌私有类,此类public继承Timer,在Timer中重新定义onTick,然后在Widget中声明一个此类型的对象。下面是这个方法的实现:
class Timer {
public:
explicit Timer(int tickFrequency);
virtual void onTick()const; //定时器每滴答一次,此函数就被自动调用一次
};
class Widget{
private:
class WidgetTimer :public Timer {
public:
virtual void onTick()const;
...
};
WidgetTimer timer;
...
};
我们派生了一个Timer的派生类WidgetTimer,并重写onTick()函数,然后定义一个WidgetTimer类对象定义于Widget
类中。相同的问题,建议使用复合模式,而不建议使用private继承,原因有两点:
① 防止Widget的派生类重写onTick()函数:
在继承方式下:如果Wiget又定义了派生类,你不希望派生类去重写onTick()函数,但是这种情况可能会无法阻止。
在复合模式下:Widget的派生类就不可能有机会去重写onTick()函数了,因为WidgetTimer类是Widget内部的一个private成员,派生类永远无法访问。
② 可以将Widget的编译依存性降至最低:
在继承方式下:如果Widget继承与Timer,那么当Widget被编译时需要知道Timer的定义(不仅仅是声明),因此你可能会在Widget的头文件中包含
#include"Timer.h"
这样的东西。在复合模式下:假设我们修改上面的复合模式,将WidgetTimer定义在Widget之外,然后在Widget内定义一个WidgetTimer的指针,此时Widget可以只带着WidgetTimer的声明式,那么当Widget编译时就不需要任何与Timer的任何东西。对大型系统而言,这是很重要的措施。
3、使用private继承比组合更加合理的例子
在派生类想要访问基类的protected部分或者想去重定义基类的虚函数的时候private继承才是有用的,但是类之间的关系是”is-implemented-in-terms-of”而不是“is-a”。然而,我同时指出有一种涉及到空间优化的边缘情况可以促使你更加喜欢private继承而不是composition(复合)。
这种情况比较激进:它只适用在没有数据的类中。这种类没有非静态数据成员;没有虚函数(因为虚函数的存在会为每个对象添加一个vptr指针,见条款7);没有虚基类(因为这样的基类同样会引入额外开销,见条款40)。从概念上来说,这样的空类对象应该不使用空间,因为对象中没有数据需要保存。然而由于技术的原因,C++使得独立对象必须占用空间。
class Empty {};
class HoldsAnint :private Empty {
private:
int x;
};
sizeof(HoldsAnint); //4
你会发现sizeof(HoldsAnInt)>sizeof(int):一个Empty数据成员也会占用空间。对于大多数编译器来说,sizeof(Empty)为1,因为C++法则处理大小为0的独立对象时会默认向” empty ”对象中插入一个char。然而,内存对齐的需求(见条款50)可能导致编译器向HoldsASnInt这样的类中添加填充物,所以HoldsAnInt对象不会只多出来一个char的大小,实际上会增加足够的空间来容纳第二个int。(在我测试过的所有编译器中,上面描述的填充也确实发生了。)
但是可能你注意到了我非常小心的说明是“独立”(freestanding)对象占用的空间必须不能为0。这个限制不能被应用在派生类对象的基类部分中,因为他们不是“独立“的。如果你继承自Empty类而不是包含一个Empty类型的对象,
class HoldsAnInt: private Empty {
private:
int x;
};
几乎可以确定sizeof(HoldsAnInt)==sizeof(int)。这被称作EBO(empty base optimization;空白基类最优化),并且我测试过的编译器都通过了这个测试。如果你是一个库开发人员,如果其客户对空间十分关心,那么了解一下EBO是很值得的。并且你需要知道EBO一般只在单继承下才是可行的。管理C++对象布局的规则通常意味着EBO不能被应用在有多个基类的派生类中。
事实上,“empty“类不是真的empty。虽然它们永远不会拥有非静态数据成员,它们通常会包含typedefs,enums,静态数据成员或者非虚函数。STL在技术上有很多包含有用成员(通常为typedefs)的空类,包括基类unary_function和binary_function,用户定义的函数对象会继承这些类。多亏了EBO的广泛使用,使得这些继承很少会增加派生类的大小。
4、牢记
-
private继承意为“is-implemented-in-terms-of(根据某物实现出)”。它通常比复合(composition)的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计时合理的。
-
和复合(composition)不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。
总结
期待大家和我交流,留言或者私信,一起学习,一起进步!