Effective C++读书笔记(25)

来源:互联网 发布:苹果mac os x 10.11 编辑:程序博客网 时间:2024/06/08 17:57

条款38:通过复合塑模出has-a或“根据某物实现出”

Model “has-a” or “is-implemented-in-terms-of”through composition

复合(composition)是类型之间的一种关系,当某种类型的对象内含另一种类型的对象,便是这种关系。例如:

classAddress { ... }; // 某人的住址

classPhoneNumber { ... };

classPerson {
public:
    ...
private:
    std::string name; // 四个合成成分物
    Address address;
    PhoneNumber voiceNumber;
    PhoneNumber faxNumber;

};

此例中,Person对象由 string,Address,和 PhoneNumber组成。在程序员之间,术语复合有很多同义词。它也可以称为layering(分层),containment(内含),aggregation(聚合),和 embedding(内嵌)。

复合有两个含意:"has-a"(有一个),和"is-implemented-in-terms-of"(根据某物实现出)。这是因为你要在你的软件中处理两个不同的领域。程序中的一些对象对应你所模拟的世界里的东西,例如人,交通工具,视频画面等等,这样的对象属于应用域部分。另外的对象纯粹是实现细节上的人工制品,例如缓冲区,mutex,搜索树等等,这些对象相当于你的软件的实现域。当复合发生在应用域的对象之间,它表达一个has-a关系;当它发生在实现域,它表达一个is-implemented-in-terms-of关系。

上面的Person示范了has-a关系。Person对象有一个名字,地址,以及语音和传真电话号码。你不能说“人是一个名字”或“人是一个地址”,你会说“人有一个名字”和“人有一个地址”。is-a和has-a不难区分。

 

比较难区分的是is-a和is-implemented-in-terms-of之间。例如,假设你需要一个template,制造一组类来表现由不重复对象组成的set。我们的第一个直觉是复用标准库中的set template。不幸的是,set的典型实现导致每个元素三个指针的开销。这是因为set通常以平衡搜索树来实现,使它们在查找,插入和删除元素时有对数时间的效率。当速度比空间更重要的时候,这是一个合理的设计,但是当空间比速度更重要时,对你的程序来说就有问题了。

作为数据结构的专家,实现 set 有诸多选择,其中一种是使用linked lists(线性链表)。标准程序库中有一个list template,所以我们决定复用它。但如果我们让新的Set template从 list 继承,即Set<T> 从 list<T> public继承,会有一个很大的错误。一个list对象可以包含重复,所以如果值3051 被插入一个 list<int> 两次,那个 list 将包含 3051 的两个拷贝。但Set 不可以包含重复,所以如果值3051被插入一个 Set<int> 两次,那个set 只包含该值的一个拷贝。因此让Set is-a(是一个)list 是不正确的,因为对 list对象成立的某些事情对Set对象不成立。

因为这两个类之间的关系不是is-a,public inheritance不是模拟这个关系的正确方法。正确的方法是认识到一个Set对象可以根据一个list对象来实现(be implemented in terms of)。

template<classT> // 将list用于Set的正确做法
class Set {
public:
    bool member(const T& item) const;
    void insert(const T& item);
    void remove(const T& item);
    std::size_t size() const;
private:
    std::list<T> rep; // 用来表述Set的数据
};

Set 的成员函数可以极大程度地依赖list和标准程序库的其它部分已经提供的机能,所以只要熟悉了用STL编程的基本方法,实现就非常简单了:

template<typenameT>
bool Set<T>::member(const T& item) const
{ return std::find(rep.begin(), rep.end(), item) != rep.end(); }

template<typenameT>
void Set<T>::insert(const T& item)
{ if (!member(item)) rep.push_back(item); }

template<typenameT>
void Set<T>::remove(const T& item)
{
    typename std::list<T>::iteratorit = std::find(rep.begin(), rep.end(), item);
    if (it != rep.end())  rep.erase(it);
}

template<typenameT>
std::size_t Set<T>::size() const
{ return rep.size(); }

这些函数足够简单,使它们成为inline的合理候选者。这个关系不是 is-a(虽然最初看上去可能很像),而是is-implemented-in-terms-of。

·    复合与public inheritance的意义完全不同。

·    在应用域,复合意味着 has-a(有一个);在实现域,复合意味着is-implemented-in-terms-of(根据某物实现出)。

 

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

Use private inheritance judiciously

C++将public inheritance视为一个is-a关系。在那个例子中,Student从Person公有继承,于是编译器在必要时刻(为了让函数调用成功),就将Student隐式转型为 Person。现在我们用privateinheritance代替publicinheritance,重写该例:

classPerson { ... };
class Student: private Person { ... };

voideat(const Person& p); // anyone can eat
void study(const Student& s); // only students study

Person p;// p is a Person
Student s; // s is a Student
eat(p); // fine, p is a Person
eat(s); // error! a Student isn't a Person

很明显,privateinheritance(私有继承)并不意味着is-a。

private inheritance的两条规则

1.     如果类之间的继承关系是private,编译器通常不会自动将派生类对象(如Student)转型为基类对象(如 Person)。这就是为什么为对象s调用eat会失败。

2.     从一个private基类继承的所有成员,在派生类中都会变成private属性,即使它们在基类中是protected或public。

private inheritance意味着 is-implemented-in-terms-of(根据某物实现出)。如果你使D 从B私有继承,用意是为了采用B中已经备妥的某些特性,而不是因为在B和D的对象之间有什么概念上的关系。privateinheritance纯粹是一种实现技术(这也就是为什么从一个私有基类继承的每一件东西在你的类中变成 private的原因:它全部都是实现的细节)。

private inheritance意味着 is-implemented-in-terms-of,好像和复合有同样的含义。怎样在它们之间做出选择?答案很简单:尽可能使用复合,必要时才使用privateinheritance。

 

假设我们工作在一个包含Widgets的应用程序上,我们需要更好地理解 Widgets 是怎样被使用的。例如,我们不仅要知道 Widget成员函数被调用的频度,还要知道调用率随着时间的流逝如何变化。带有多个执行阶段的程序在不同的执行阶段可以有不同的行为轮廓。例如,编译器在解析阶段所使用的函数,大大不同于优化和代码生成阶段所使用的函数。

我们决定修改Widget类,让它记录每个成员函数被调用的次数。在运行时,我们可以周期性地检查这一信息,也许再加上每个Widget的值,以及我们需要评估的任何其它数据。为了进行这项工作,我们需要设立某种定时器,使我们知道收集统计数据的时候是否到了。

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

为了让Widget重新定义Timer中的virtual函数,Widget必须从Timer继承。但是public inheritance在这种情况下不合适,因为Widget并不是个Timer。我们就必须以private形式继承Timer:

classWidget: private Timer {
private:
    virtual void onTick() const; // 查看Widget数据,等等
    ...
};

通过private inheritance,Timer的public onTick 函数在Widget中变成 private,而且在我们重新声明它的时候,也把它保留在那里。将 onTick 放入public接口将误导客户认为他们可以调用它,而这违背了“让接口容易被正确使用,不易被误用”。

在这里,privateinheritance并不是绝对必要的。如果我们可以用复合来代替。只需在Widget内声明一个嵌套的private类,后者以public形式继承Timer并重新定义onTick,然后将这样一个类型的对象放于Widget内。以下就是这个方法的概要:

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

    ...
};

这个设计比只用privateinheritance更复杂,因为它包括 public inheritance和复合两者,以及一个新class(WidgetTimer) 的引入。下面给出两条为什么“更愿意用publicinheritance加复合而不用privateinheritance”的两个原因:

1.     你可能要做出允许Widget有派生类的设计,但是还要禁止派生类重新定义onTick。如果Widget从Timer继承,那是不可能的(派生类可以重定义virtual函数,即使调用它们是不被允许的);但是如果WidgetTimer在Widget中是private且从Timer继承,Widget 的派生类就不能访问WidgetTimer,因此就不能从它继承或重定义它的virtual函数。(WidgetTimer夹在Widget和Timer之间,使得Widget不能继承Timer中的onTick)

2.     最小化Widget的编译依赖。如果Widget从Timer继承,在Widget被编译时,Timer 的定义必须可见,所以定义Widget的文件可能不得不#include Timer.h;另一方面,如果 WidgetTimer移出Widget,而Widget内含一个指针指向WidgetTimer,Widget 就可以只需要WidgetTimer类的一个简单声明,不再需要#include任何与Timer有关的东西。

 

private inheritance主要用武之地是“当一个将要成为派生类的类需要访问将要成为基类的类的protected成分,或者希望重新定义一个或多个virtual函数”。但是这里类之间的关系是is-implemented-in-terms-of,而不是is-a。然而,有一种激进情况涉及空间最优化,可能会使你倾向于private inheritance,而不是“继承+复合”。

这个激进情况仅仅适用于你处理一个其中没有数据的类的时候。这样的类没有 non-static成员变量、virtual函数或virtual基类。在理论上,这样的空类对象应该不占用空间,因为没有 任何隶属对象的数据需要存储。然而,由于技术上的原因,C++裁定凡是独立(非附属)对象都必须有非零大小,所以如果你这样做:

classEmpty {}; // 没有数据,所以其对象应该不使用任何内存
class HoldsAnInt { // 应该只需要一个int空间
private:
    int x;
    Empty e; // 应该不需要任何内存
};

你将发现 sizeof(HoldsAnInt) > sizeof(int),也就是说一个Empty成员变量要求内存。对以大多数编译器,sizeof(Empty)是1,通常C++会默默安插一个 char 到空对象内。然而,齐位需求(alignment)可能促使编译器向类似HoldsAnInt的类中增加填充物,所以很可能HoldsAnInt对象得到的不仅仅是一个char的大小,实际上它们可能会扩张到足够又存放一个int。

但如果你用从Empty私有继承代替包含一个此类型的对象:

classHoldsAnInt: private Empty {
private:
    int x;
};

几乎可以确定sizeof(HoldsAnInt) ==sizeof(int),这就是所谓的EBO(empty base optimization,空白基类最优化)。如果程序空间敏感,EBO就值得了解。同样值得了解的是EBO通常只在单继承下才可行。

现实中的"empty"class并不真的为空。虽然它们从未拥有non-static成员变量,但经常会包含typedefs,enums,static成员变量,或non-virtual函数。STL有很多技术用途的emptyclasses,包括基类unary_function和binary_function,用户定义函数对象通常从这些类继承而来。EBO的普遍实现,使得这样的继承很少增加派生类的大小。

 

小结:

1.    与复合不同,private inheritance能使空白基类最优化(EBO),这对致力于最小化对象大小的库开发者来说可能是很重要的。但大多数类不是空的,所以EBO很少会成为private inheritance的正当理由。

2.     大多数继承相当于 is-a,而这正是public inheritance而非private inheritance所做的事。

3.     复合和private inheritance都意味着 is-implemented-in-terms-of,但是复合更易于理解,所以你应该尽你所能使用它。

4.     privateinheritance更可能在以下情况中成为一种设计策略:当你要处理的两个类不具有 is-a的关系,而且其中的一个还需要访问另一个的protected成员或需要重定义一个或更多个它的 virtual函数。甚至在这种情况下,我们也看到publicinheritance和复合的混合使用通常也能产生你想要的行为,虽然有更大的设计复杂度。

5.     在考虑过所有其他方案之后,如果依然认为private继承是“表现程序内两个类之间的关系”的最佳方法,这才用它。

原创粉丝点击