effective c++ 39 明智而审慎地使用private继承

程序员小x大约 4 分钟C++C++effective c++读书笔记

effective c++ 39 明智而审慎地使用private继承

什么是private继承?

下面例子中,我们称类Student private继承类Person。

class Person {...};
class Student : private Person {...}; // private继承

根据条款32,public继承意味着is-a关系,那么private继承意味着什么?

两点: 1)编译器不会自动将一个derived class对象转换为base class对象;

2)由private base class继承而来的所有成员,在derived class中都会变成private属性,即使它们在base class中原本是protected或public属性;

对于2)不过多解释,对于1),我们看下面的例子:

void eat(const Person& p);
void study(cosnt Student& s);

Person p;  // p是人
Student s; // s是学生

eat(p); // OK:p是人,会吃
eat(s);     // 编译器报错:因为Student私有继承自Person,编译器不会将Student

对象自动转换为Person对象 我们会看到,编译器并不会将一个private继承关系中的derived 对象自动转换为base对象。

private继承与implemented-in-terms-of(根据某物实现) 如果让class D以private继承class B,用意是为了采用class B内已经准备妥当的某些特性,不是因为B对象和D对象在存在任何观念上的关系。private继承纯粹是一种实现技术,根据条款34,private继承意味着只有实现部分被继承,接口部分应略去。 如果D private继承B,i.e. D对象根据B对象实现而得,没有其他意涵。private继承在软件“设计”层面没有意义,只存在软件实现层面。

既然private意味着implemented-in-terms-of(根据某物实现),条款38复合(composition)也是如此,那么如何选择? 答:尽可能使用复合,必要时才使用private继承。什么时候才是必要?主要是当protected成员或virtual函数牵扯进来的时候,因为通过复合只能访问public成员函数和变量,除非降低类封装性(使用友元或者添加public接口)。下文提到的极端情况,也适用private继承。

如何使用private继承?

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

我们发现有个Timer class,可以复用既有代码

class Timer {
public:
    explicit Timer(int tickFrequency);
    virtual void onTick() const; // 定时器每滴答一次, 就调用一次该函数
    ...
};

1)使用private继承

为了让Widget重新定义Timer内的virtual函数,Widget可以继承自Timer。由于Widget并不是Timer,因此不能使用public继承,只能使用private继承。

class Widget: private Timer {
private:
virtual void onTick() const; // 周期性执行, 查看Widget数据等
};

2)使用复合

然而该设计有一个缺陷:无法阻止derived class重新定义onTick(不论private继承,还是public继承)。改用复合+public继承的方法,在Widget内声明一个嵌套private class,后者以public形式继承Timer并重新定义onTock。

class Widget {
private:
    class WidgetTimer: public Timer { // 内嵌类public继承Timer
    public:
        virtual void onTick() const;
        ...
    };
    WidgetTimer timer;
    ...
};

这样设计有2个优点: (1)能解决private继承无法解决的derived class重新定义onTick问题; (2)如果想要将Widget编译依存性降至最低,可以将WidgetTimer移除Widget类外,在Widget类内只需要一个指针指向WidgetTimer即可,头文件中不再需要include Timer或者WidgetTimer,而只需要class声明。

极端情况

当一个类是空类时(没有任何non-static成员变量,virtual函数)。如果一个类含有一个Empty,可能会导致占用的内存空间变大。比如:

class Empty { }; // 空类占用1byte空间
class HoldsAnInt {
private:
    int x; // 4byte
    Empty e; // 1byte,实际可能占用4byte(对齐)
};

理论上,空类应该不占用空间,但实际上C++实现,空类占用1byte,即sizeof(Empty) = 1。这样,HoldsAnInt原本只应该占用4byte,实际上可能占用5byte或者8byte(如果有4byte对齐要求)。

如果是private继承,就能解决这个问题。下面例子中,sizeof(HoldsAnInt) = 4。

class HoldsAnInt: private Empty { // private继承
private:
    int x; // 4byte
};

总结

  • private继承意味着根据某物实现(is-implemented-in-terms-of)。它通常比组合的级别低。但是当derived class需要访问protected base class成员,或者需要重新定义继承而来的virtual函数时,这么设计是合理的。
  • 和复合不同,private继承可以造成empty base最优化。这对致力于对象尺寸最小化的程序库开发者而言,可能很重要。
Loading...