Effective C++笔记: 继承和面向对象设计(三)

来源:互联网 发布:中国电信网络发展部 编辑:程序博客网 时间:2024/06/05 21:12

 

Item 38: 通过 composition(复合)塑模出 "has-a" "is-implemented-in-terms-of"(根据某物实现出)

composition(复合)是类型之间的一种关系,表示某种类型的对象内含其他类型的对象。例如:

class Address { ... };             // where someone lives

class PhoneNumber { ... };

class Person {
public:
  ...

private:
 
std::string name;               // composed object
 
Address address;                // ditto
 
PhoneNumber voiceNumber;        // ditto
 
PhoneNumber faxNumber;          // ditto
};

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

 

区分has-a is-implemented-in-terms-of

上面的 Person class(类)示范了 has-a(有一个)的关系。一个 Person object(对象)has a(有一个)名字,一个地址,以及语音和传真电话号码。你不能说一个人 is a(是一个)名字或一个人 is an(是一个)地址。你可以说一个人 has a(有一个)名字和 has an(有一个)地址。

is-implemented-in-terms-of的例子是,比如,你需要一个类的模板来表现相当小的 objects(对象)的 sets,也就是说,排除重复的集合。你决定复用标准的 C++ 库中list template(模板),具体地说,你决定让你的新的 Set template(模板)从 list 继承。也就是说,Set<T> 将从 list<T> 继承。毕竟,在你的实现中,一个 Set object(对象)实际上就是一个 list object(对象)。于是,你就像这样声明你的 Set template(模板):

template<typename T>                       // the wrong way to use list for Set
class Set: public std::list<T> { ... };

在这里,看起来每件事情都很好。但实际上有一个很大的错误。就像 Item 32 中的解释,如果 D is-a(是一个)B对于 B 成立的每一件事情对 D 也成立。然而,一个 list object(对象)可以包含重复,所以如果值 3051 被插入一个 list<int> 两次,那个 list 将包含 3051 的两个拷贝。与此对照,一个 Set 不可以包含重复,所以如果值 3051 被插入一个 Set<int> 两次,那个 set 只包含该值的一个拷贝。因此一个 Set is-a(是一个)list 是不正确的,因为对 list objects(对象)成立的某些事情对 Set objects(对象)不成立。

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

template<class T>                   // the right way to use list for 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;                 // representation for Set data
};

 

总结:

composition(复合)与 public inheritance(公有继承)的意义完全不同。

application domain(应用领域)中,composition(复合)意味着 has-a(有一个)。在 implementation domain(实现领域)中意味着 is-implemented-in-terms-of(是根据……实现的)。

 

Item 39: 谨慎使用 private inheritance(私有继承)

Item 32 论述了 C++ public inheritance(公有继承)视为一个 is-a 关系。当给定一个 hierarchy(继承体系),其中有一个 class Student 从一个 class Person 公有继承,编译器在必要时刻(为了成功调用一个函数)要将 Students 隐式转型为 Persons。但对private inheritance(私有继承)情况会如何呢?

class Person { ... };
class Student: private Person { ... };     // inheritance is now private

void eat(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

很明显,private inheritance(私有继承)不意味着 is-a。那么它意味着什么呢?

 

private inheritance(私有继承)的两个规则:

1. public inheritance(公有继承)相反,如果 classes(类)之间的 inheritance relationship(继承关系)是 private(私有)的,编译器通常不会将一个 derived class object(派生类对象)(诸如 Student)转型为一个 base class object(基类对象)(诸如 Person)。这就是为什么为 object(对象)s 调用 eat 会失败。

2.从一个 private base class(私有基类)继承的 members(成员)会成为 derived class(派生类)的 private members(私有成员),即使它们在 base class(基类)中是 protected(保护)的或 public(公有)的。

 

private inheritance(私有继承)意味着 is-implemented-in-terms-of(是根据……实现的)。如果你使 class(类)D class(类)B 私有继承,你这样做是因为你对于利用在 class(类)B 中才可用的某些特性感兴趣,而不是因为在 types(类型)B types(类型)D objects(对象)之间有什么概念上的关系。同样地,private inheritance(私有继承)纯粹是一种实现技术。(这也就是为什么你从一个 private base class(私有基类)继承的每一件东西都在你的 class(类)中变成 private(私有)的原因:它全部都是实现的细节。)利用 Item 34 中提出的条款,private inheritance(私有继承)意味着只有 implementation(实现)应该被继承;interface(接口)应该被忽略。如果 D B 私有继承,它就意味着 D objects are implemented in terms of B objectsD 对象是根据 B 对象实现的),没有更多了。private inheritance(私有继承)在 software design(软件设计)期间没有任何意义,只在 software implementation(软件实现)期间才有。

 

私有继承与复合:

private inheritance(私有继承)意味着 is-implemented-in-terms-of(是根据……实现的)的事实有一点混乱,因为 Item 38 指出 composition(复合)也有同样的含义。你怎么预先在它们之间做出选择呢?答案很简单:尽可能使用composition(复合),只有在绝对必要的时候才用 private inheritance(私有继承)。

 

例如,我们要给Widget class加入一个Tick功能,假设我们已经有了一个Timer class

class Timer {
public:
  explicit Timer(int tickFrequency);
   virtual void onTick() const;          // automatically called for each tick
  ...
};

为了给 Widget 重定义 Timer 中的一个 virtual function(虚拟函数),Widget 必须从 Timer 继承。但是 public inheritance(公有继承)在这种情况下不合适。Widget is-a(是一个)Timer 不成立。Widget 的客户不应该能够在一个 Widget 上调用 onTick,因为在概念上那不是的 Widget  interface(接口)的一部分。允许这样的函数调用将使客户更容易误用 Widget interface(接口),这是一个对 Item 18 的关于使接口易于正确使用,而难以错误使用的建议的明显违背。public inheritance(公有继承)在这里不是正确的选项。

因此我们就 inherit privately(秘密地继承):

class Widget: private Timer {
private:
  virtual void onTick() const;           // look at Widget usage data, etc.
  ...
};

通过 private inheritance(私有继承)的能力,Timer public(公有)onTick 函数在 Widget 中变成 private(私有)的,而且在我们重新声明它的时候,也把它保留在那里。重复一次,将 onTick 放入 public interface(公有接口)将误导客户认为他们可以调用它,而这违背了 Item 18

这是一个很好的设计,但值得一提的是,private inheritance(私有继承)并不是绝对必要的。如果我们决定用 composition(复合)来代替,也是可以的。我们仅需要在我们从 Timer 公有继承来的 Widget 内声明一个 private nested class(私有嵌套类),在那里重定义 onTick,并在 Widget 中放置一个那个类型的 object(对象)。以下就是这个方法的概要:

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

public inheritance(公有继承)加 composition(复合)而不用 private inheritance(私有继承)的两个原因。

首先,你可能要做出允许 Widget derived classes(派生类)的设计,但是你还可能要禁止 derived classes(派生类)重定义 onTick。如果 Widget Timer 继承,那是不可能的,即使 inheritance(继承)是 private(私有)的也不行。(回忆 Item 35 derived classes(派生类)可以重定义 virtual functions(虚拟函数),即使调用它们是不被允许的。)但是如果 WidgetTimer Widget 中是 private(私有)的而且是从 Timer 继承的,Widget derived classes(派生类)就不能访问 WidgetTimer,因此就不能从它继承或重定义它的 virtual functions(虚拟函数)。如果你曾在 Java C# 中编程并且错过了禁止 derived classes(派生类)重定义 virtual functions(虚拟函数)的能力(也就是,Java final methods(方法)和 C# sealed),现在你有了一个在 C++ 中的到类似行为的想法。

第二,你可能需要最小化 Widget compilation dependencies(编译依赖)。如果 Widget Timer 继承,在 Widget 被编译的时候 Timer definition(定义)必须是可用的,所以定义 Widget 的文件可能不得不 #include Timer.h。另一方面,如果 WidgetTimer 移出 Widget Widget 只包含一个指向一个 WidgetTimer pointer(指针),Widget 就可以只需要 WidgetTimer class(类)的一个简单的 declaration(声明);为了使用 Timer 它不需要 #include 任何东西。对于大型系统,这样的隔离可能非常重要(关于 minimizing compilation dependencies(最小化编译依赖)的细节,参见 Item 31)。

 

EBO empty base optimization空白基类最优化)

一个空类是指一个类,没有 non-static data members(非静态数据成员);没有 virtual functions(虚函数)(因为存在这样的函数会在每一个 object(对象)中增加一个 vptr ——参见 Item 7);也没有 virtual base classes(虚拟基类)(因为这样的 base classes(基类)也会引起 size overhead(大小成本)——参见 Item 40)。在理论上,这样的 empty classes(空类)的 objects(对象)应该不占用空间,因为没有 per-object(逐对象)的数据需要存储。然而,由于 C++ 天生的技术上的原因,freestanding objects(独立对象)必须有 non-zero size(非零大小),所以如果你这样做,

class Empty {};                      // has no data, so objects should
                                     // use no memory
class HoldsAnInt {                   // should need only space for an int
private:
  int x;
  Empty e;                           // should require no memory
};

你将发现 sizeof(HoldsAnInt) > sizeof(int);一个 Empty data member(空数据成员)需要存储。对以大多数编译器,sizeof(Empty) 1,这是因为 C++ 法则反对 zero-size freestanding objects(独立对象)一般是通过在 "empty" objects对象)中插入一个 char 完成的。然而,alignment requirements(对齐需求)(参见 Item 50)可能促使编译器向类似 HoldsAnInt classes(类)中增加填充物,所以,很可能 HoldsAnInt objects 得到的不仅仅是一个 char 的大小,实际上它们可能会扩张到足以占据第二个 int 的位置。(在我测试过的所有编译器上,这毫无例外地发生了。)

但是也许你已经注意到我小心翼翼地说 "freestanding" objects独立对象)必然不会有 zero size。这个约束不适用于 base class parts of derived class objects(派生类对象的基类构件),因为它们不是独立的。如果你用从 Empty 继承代替包含一个此类型的 object(对象),

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

你几乎总是会发现 sizeof(HoldsAnInt) == sizeof(int)

在实践中,"empty" classes类)并不真的为空。虽然他们绝对不会有 non-static data members(非静态数据成员),但它们经常会包含 typedefsenums(枚举),static data members(静态数据成员),或 non-virtual functions(非虚拟函数)。STL 有很多包含有用的 members(成员)(通常是 typedefs)的专门的 empty classes(空类),包括 base classes(基类)unary_function binary_functionuser-defined function objects(用户定义函数对象)通常从这些 classes(类)继承而来。感谢 EBO 的普遍实现,这样的继承很少增加 inheriting classes(继承来的类)的大小。

 

总结:

private inheritance(私有继承)意味着 is-implemented-in-terms of(是根据……实现的)。它通常比 composition(复合)更低级,但当一个 derived class(派生类)需要访问 protected base class members(保护基类成员)或需要重定义 inherited virtual functions(继承来的虚拟函数)时,这么设计是合理的。

composition(复合)不同,private inheritance(私有继承)能使 empty base optimization(空基优化)有效。这对于致力于最小化 object sizes(对象大小)的库开发者来说可能是很重要的。

原创粉丝点击