您的位置:首页 > 编程语言 > C语言/C++

[翻译] Effective C++, 3rd Edition, Item 31: 最小化文件之间的 compilation dependencies(编译依赖)(下)

2005-09-05 00:31 543 查看
(点击此处,接上篇)

当 object references(引用)和 pointers(指针)可以做到时就避免使用 objects。仅需一个类型的声明,你就可以定义到这个类型的 references 和 pointers。而定义一个类型的 objects 必须要存在这个类型的定义。

只要你能做到,就用对 class declarations(类声明)的依赖替代对 class definitions(类定义)的依赖。注意在你声明一个使用一个 class 的函数时绝对不需要有这个 class definition,即使这个函数通过传值方式传递或返回这个 class:

class Date; // class declaration

Date today(); // fine — no definition
void clearAppointments(Date d); // of Date is needed

当然,pass-by-value(传值)通常不是一个好主意(参见 Item 20),但是如果你发现你自己因为某种原因而使用它,依然不能为引入不必要的 compilation dependencies(编译依赖)辩解。

不定义 Date 就可以声明 today 和 clearAppointments 的能力可能会令你感到惊奇,但是它其实并不像看上去那么不同寻常。只有有人调用了这些函数,Date 的定义才必须在调用之前被看到。为什么费心去声明没有人调用的函数,你觉得奇怪吗?很简单。并不是没有人调用它们,而是并非每个人都要调用它们。如果你有一个包含很多 function declarations(函数声明)的库,每一个客户都要调用每一个函数是不太可能的。通过将提供 class definitions(类定义)的责任从你的 function declarations(函数声明)的头文件转移到客户的包含 function calls(函数调用)的文件,你就消除了客户对他们并不真正需要的 type definitions(类型定义)的人为依赖。

为 declarations(声明)和 definitions(定义)分别提供头文件。为了便于坚持上面的指导方针,头文件需要成对出现:一个用于 declarations(声明),另一个用于 definitions(定义)。当然,这些文件必须保持一致。如果一个 declaration(声明)在一个地方被改变了,它必须在两处都被改变。得出的结果是:库的客户应该总是 #include 一个 declaration(声明)文件,而不是自己 forward-declaring(前向声明)某些东西,而库的作者应该提供两个头文件。例如,想要声明 today 和 clearAppointments 的 Date 的客户不应该像前面展示的那样手动前向声明 Date。更合适的是,它应该 #include 适当的用于 declarations(声明)的头文件:

#include "datefwd.h" // header file declaring (but not
// defining) class Date

Date today(); // as before
void clearAppointments(Date d);

declaration-only(仅有声明)的头文件的名字 "datefwd.h" 基于来自标准 C++ 库(参见 Item 54)的头文件 <iosfwd>。<iosfwd> 包含 iostream 组件的 declarations(声明),而它们相应的 definitions(定义)在几个不同的头文件中,包括 <sstream>,<streambuf>,<fstream> 和 <iostream>。

<iosfwd> 在其它方面也有启发意义,而且它表明本 Item 的建议对于 templates(模板)和 non-templates(非模板)一样有效。尽管 Item 30 解释了在很多构建环境中,template definitions(模板定义)的典型特征是位于头文件中,但有些环境允许 template definitions(模板定义)位于非头文件中,所以为模板提供一个 declaration-only(仅有声明)的头文件依然是有意义的。<iosfwd> 就是一个这样的头文件。

C++ 还提供了 export 关键字允许将 template declarations(模板声明)从 template definitions(模板定义)中分离出来。不幸的是,支持 export 的编译器非常少见,而与 export 打交道的实际经验就更少了。结果是,现在就说 export 在高效 C++ 编程中扮演什么角色还为时尚早。

像 Person 这样的使用 pimpl idiom(惯用法)的 classes 经常被称为 Handle classes。为了避免你对这样的 classes 实际上做什么事的好奇心,一种方法是将所有对它们的函数调用都转送给相应的 implementation classes(实现类),而让那些 classes 来做真正的工作。例如,这是两个 Person 的 member functions(成员函数)被实现的例子:

#include "Person.h" // we're implementing the Person class,
// so we must #include its class definition

#include "PersonImpl.h" // we must also #include PersonImpl's class
// definition, otherwise we couldn't call
// its member functions; note that
// PersonImpl has exactly the same
// member functions as Person — their
// interfaces are identical

Person::Person(const std::string& name, const Date& birthday,
const Address& addr)
: pImpl(new PersonImpl(name, birthday, addr))
{}

std::string Person::name() const
{
return pImpl->name();
}

注意 Person 的 constructor(构造函数)是如何调用 PersonImpl 的 constructor(构造函数)的(通过使用 new ——参见 Item 16),以及 Person::name 是如何调用 PersonImpl::name 的。这很重要。使 Person 成为一个 Handle class 不需要改变 Person 要做的事情,仅仅是改变了它做事的方法。

另一个不同于 Handle class 的候选方法是使 Person 成为一个被叫做 Interface class 的特殊种类的 abstract base class(抽象基类)。这样一个 class 的作用是为 derived classes(派生类)指定一个 interface(接口)(参见 Item 34)。结果,它的典型特征是没有 data members(数据成员),没有 constructors(构造函数),有一个 virtual destructor(虚拟析构函数)(参见 Item 7)和一组指定 interface(接口)的 pure virtual functions(纯虚拟函数)。

Interface classes 类似 Java 和 .NET 中的 Interfaces,但是 C++ 并不会为 Interface classes 强加那些 Java 和 .NET 为 Interfaces 强加的种种限制。例如,Java 和 .NET 都不允许 Interfaces 中有 data members(数据成员)和 function implementations(函数实现),但是 C++ 不禁止这些事情。C++ 的较大弹性是有用处的。就像 Item 36 解释的,在一个 hierarchy(继承体系)的所有 classes 中 non-virtual functions(非虚拟函数)的实现应该相同,因此将这样的函数实现为声明它们的 Interface class 的构件就是有意义的。

一个 Person 的 Interface class 可能就像这样:

class Person {
public:
virtual ~Person();

virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
virtual std::string address() const = 0;
...
};

这个 class 的客户必须针对 Person 的指针和引用进行编程,因为实例化包含 pure virtual functions(纯虚拟函数)的 classes 是不可能的。(然而,实例化从 Person 派生的 classes 是可能的——参见后面。)和 Handle classes 的客户一样,除非 Interface class 的 interface(接口)发生变化,否则 Interface classes 的客户不需要重新编译。

一个 Interface class 的客户必须有办法创建新的 objects。他们一般通过调用一个为“可以真正实例化的 derived classes(派生类)”扮演 constructor(构造函数)角色的函数做到这一点的。这样的函数一般称为 factory functions(参见 Item 13)或 virtual constructors(虚拟构造函数)。他们返回指向动态分配的支持 Interface class 的 interface 的 objects 的指针(smart pointers(智能指针)更合适——参见 Item 18)。这样的函数在 Interface class 内部一般声明为 static:

class Person {
public:
...

static std::tr1::shared_ptr<Person> // return a tr1::shared_ptr to a new
create(const std::string& name, // Person initialized with the
const Date& birthday, // given params; see Item 18 for
const Address& addr); // why a tr1::shared_ptr is returned
...
};

客户就像这样使用它们:

std::string name;
Date dateOfBirth;
Address address;
...

// create an object supporting the Person interface
std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));

...

std::cout << pp->name() // use the object via the
<< " was born on " // Person interface
<< pp->birthDate()
<< " and now lives at "
<< pp->address();
... // the object is automatically
// deleted when pp goes out of
// scope — see Item 13

当然,在某些场合,必须定义支持 Interface class 的 interface(接口)的 concrete classes (具体类)并调用真正的 constructors(构造函数)。这所有的一切都发生在幕后,隐藏在那个包含了 virtual constructors(虚拟构造函数)的实现的文件之内。例如,Interface class Person 可以有一个 concrete derived class(具体派生类)RealPerson,它为继承到的 virtual functions(虚拟函数)提供了实现:

class RealPerson: public Person {
public:
RealPerson(const std::string& name, const Date& birthday,
const Address& addr)
: theName(name), theBirthDate(birthday), theAddress(addr)
{}

virtual ~RealPerson() {}

std::string name() const; // implementations of these
std::string birthDate() const; // functions are not shown, but
std::string address() const; // they are easy to imagine

private:
std::string theName;
Date theBirthDate;
Address theAddress;
};

给出了 RealPerson,写 Person::create 就微不足道了:

std::tr1::shared_ptr<Person> Person::create(const std::string& name,
const Date& birthday,
const Address& addr)
{
return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday,addr));
}

Person::create 的一个更现实的实现会依赖于诸如,另外的函数参数的值,从文件或数据库读出的数据,环境变量等等,创建不同 derived class(派生类)类型的 objects。

RealPerson 示范了实现一个 Interface class 的两种最通用的机制中的一种:从 Interface class(Person)继承它的 interface specification(接口规格),然后实现 interface(接口)中的函数。实现一个 Interface class 的第二种方法包含 multiple inheritance(多继承),在 Item 40 中探讨这个话题。

Handle classes 和 Interface classes 从 implementations(实现)中分离出 interfaces(接口),因此减少了文件之间的编译依赖。如果你是一个喜好挖苦的人,我知道你正在找小号字体写成的限制。“所有这些把戏会骗走我什么呢?”你小声嘀咕着。答案在计算机科学中非常平常:它会消耗一些运行时的速度,另外每个 object 会占用一些额外的内存。

在 Handle classes 的情况下,member functions 必须通过 implementation pointer(实现的指针)得到 object 的数据。这就在每次访问中增加了一个间接层。而且你必须在存储每一个 object 所需的内存量中增加这一 implementation pointer(实现的指针)的大小。最后,这一 implementation pointer(实现的指针)必须被初始化(在 Handle class 的 constructors(构造函数)中)为指向一个动态分配的 implementation object(实现的对象),所以你要承受动态内存分配(以及随后的释放)所固有的成本和遭遇 bad_alloc (out-of-memory) exceptions(异常)的可能性。

对于 Interface classes,每一个函数调用都是虚拟的,所以你每调用一次函数就要支付一个间接跳转的成本(参见 Item 7)。还有,从 Interface class 派生的 objects 必须包含一个 virtual table pointer(还是参见 Item 7)。这个 pointer 可能增加存储一个 object 所需的内存量,这依赖于这个 Interface class 是否是这个 object 的 virtual functions(虚拟函数)的唯一来源。

最后,无论 Handle classes 还是 Interface classes 都不能大量使用 inline functions(内联函数)。Item 30 解释了为什么一般情况下函数本体必须在头文件中才能做到 inline,但是 Handle classes 和 Interface classes 一般都被设计用于隐藏类似函数本体这样的实现细节。

然而,仅仅因为它们所涉及到的成本而放弃 Handle classes 和 Interface classes 会成为一个严重的错误。virtual functions(虚拟函数)也是一样,但你还是不能放弃它们,对吗?(如果能,你看错书了。)替代做法是,考虑以一种改进的方式使用这些技术。在开发过程中,使用 Handle classes 和 Interface classes 来最小化当实现发生变化时对客户的影响。当能看出在速度和/或大小上的不同足以证明增加 classes 之间的耦合是值得的时候,可以用 concrete classes(具体类)取代 Handle classes 和 Interface classes 供产品使用。

Things to Remember

最小化编译依赖后面的一般想法是用对 declarations(声明)的依赖取代对 definitions(定义)的依赖。基于此想法的两个方法是 Handle classes 和 Interface classes。

库头文件应该以完整并且 declaration-only(只有声明)的形式存在。无论是否包含 templates(模板)都适用于这一点。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐