Effective C++条款39:明智而审慎地使用private继承(Use private inheritance judiciously)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

Effective C++条款39明智而审慎地使用private继承Use private inheritance judiciously


《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)。这被称作EBOempty 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最优化。这对致力于“对象尺寸最小化”的程序库开发者而言可能很重要。

总结

期待大家和我交流留言或者私信一起学习一起进步

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++

“Effective C++条款39:明智而审慎地使用private继承(Use private inheritance judiciously)” 的相关文章