effective C++ 条款 39:明智而审慎地使用private继承

c++中public继承视为is-a关系。现在看private继承:

class Person{...};
class Student: private Person {...};
void eat(const Person& p);
void study(const Student& s);

Person p;
Student s;
eat(p);
eat(s); //错误! 难道学生不是人?!

显然private继承不是is-a关系。

由private base class继承而来的所有成员,在derived class中都会成为private属性,纵使它们在base class中原本是protected或public。private继承意味着implemented-in-term-of(根据某物实现)。

如果class D以private形式继承class B,用意是为了采用class B内已经备妥的某些特性,不是因为B对象和D对象存在任何观念上的关系。private继承纯粹只是一种实现技术(这就是为什么继承自private base class的每样东西在你的class内都是private:因为他们都是实现细节而已)。用条款34的术语说,private继承意味着只有实现部分被继承,接口部分应略去。如果D以private形式继承B,意思是D对象根据B对象实现而得。private继承在软件“设计”层面上没有意义,其意义只及于软件实现层面。

条款38说复合(composition)的意义也是is-implemented-in-term-of,如何进行取舍?尽可能的使用复合,必要时才使用private继承:主要是当一个意欲成为derived class者想访问一个意欲成为base class者的protected成分,或为了重新定义virtual函数还有一种激进情况是空间方面的厉害关系

有个Widget class,它记录每个成员函数的被调用次数。运行期周期性的审查那份信息。为了完成这项工作,我们需要设定某种定时器,使我们知道收集统计数据的时候是否到了。

为了复用既有代码,我们发现了Timer:

class Timer {
public:
    explicit Timer(int tickFrequency);
    virtual void onTick() const;
};

每次滴答就调用某个virtual函数,我们可以重新定义那个virtual函数,让后者取出Widget的当时状态。

为了让Widget重新定义Timer内的virtual函数,Widget必须继承自Timer。但public继承并不适当,因为Widget并不是一个Timer。不能够对一个Widget调用onTick吧,观念上那并不是Wigdet接口的一部分。

我们必须用private继承Timer:

class Widget: private Timer{
private:
    virtual void onTick() const;
};

再说一次,把onTick放进public接口内会导致客户以为他们可以调用它,那就违反了条款18.

这个设计好,但不值几文钱,private继乘并非绝对必要。如果我们决定用复合取而代之,是可以的,只要在Widget内声明一个嵌套式private class,后者以public形式继承Timer并重新定义onTick,然后放一个这种类型的对象在Widget内:

class Widget{
private:
    class WidgetTimer: public Timer{
    public:
        virtual void onTick() const;
    };
    WidgetTimer timer;
};

这个设计比只是用private继承复杂一些,但是有两个理由可能你愿意或应该选择这样的public继承加复合:

首先,或许会想设计Widget使它得以拥有derived classes,但同时你可能会想阻止derived clssses 重新定义onTick。如果Widget继承自Timer,上面的想法就不可能实现,即使是private继承也不可能。(条款35说derived class可以重新定义virtual函数,即使它们不得调用它)但如果WidgetTimer是Widget内部的一个private成员并继承Timer,Widget的derived classes将无法取用WidgetTimer,因此无法继承它或重新定义它的virtual函数。有些类似java的final或c#的sealed。

第二,或许想要将Widget的编译依存性降至最低,若Widget继承Timer,当Widget被编译时,timer的定义必须可见,所以定义Widget的那个文件必须包含Timer.h。但如果WidgetTimer移除Widget之外而Widget内含一个指针指向WidgetTimer,Widget可以只带着一个简单的WidgetTimer声明式,不再需要#include任何与timer有关的东西。对大型系统而言,如此的解耦(decouplings)可能是重要的措施。

还有一种激进情况,只适用于你所处理的class不带任何数据时。这样的classes没有non-static成员变量,没有virtual函数(这种函数会为每个对象带来一个vptr),也没有virtual base classes(这样的base classes 也会招致体积上的额外开销,见条款40)。这种所谓的empty class对象不使用任何空间,因为没有任何隶属对象的数据要存储,然而由于技术上的理由,c++裁定凡是独立(非附属)对象都必须有非零大小:

class Empty{};
class HoldsAnInt {
private:
    int x;
    Empty e; //应该不需要任何内存
};

你会发现sizeof(HoldsAnInt)> sizeof(int);一个Empty成员变量竟然要求内存。在多数编译器中sizeof(Empty)获得1,因为面对“大小为零的独立对象”,通常c++官方勒令默默安插一个char到空对象内。然而齐位需求(alignment)可能造成编译器为类似HoldsAnInt这样的class加上一些衬垫(padding),所以有可能HoldsAnInt对象不只多一个char大小,实际上放大到多一个int。

独立(非附属)这个约束不适用于derived class对象内的base class成分,因为它们并非独立。如果你继承Empty,而不是内含一个那种类型的对象:

class HoldsAnInt: private Empty{
private:
    int x;
};

几乎可以确定sizeof(HoldsAnInt) == sizeof(int)。这是所谓的EBO(empty base optimization:空白基类最优化),如果你是一个库开发成员,而你的客户非常在意空间,那么值得注意EBO。另外一个值得知道的是,一般EBO只在单一继承(而非多继承)下才可行。

现实中的“Empty”class并不是真的empty。虽然他们从未拥有non-static 成员变量,却往往内含typedefs, enums, static成员变量,或non-virtual函数。stl就有许多技术用途的empty classes,其中内含有用的成员(通常是typedefs),包括base classes unary_function和binary_function,这些是“用户自定义的函数对象”通常都会继承的classes。感谢EBO的广泛实践,这样的继承很少增加derived classes的大小。

尽管如此,大多数class并非empty,所以EBO很少成为private继承的正当理由。复合和private继承都意味着is-implemented-in-term-of,但复合比较容易理解,所以无论什么时候,只要可以,还是应该选择复合。

原文地址:https://www.cnblogs.com/lidan/p/2350529.html