《Effective C++》:条款31:将文件间的编译依存关系降至最低

假如你在修改程序,只是修改了某个class的接口的实现,而且修改的是private部分。之后,你编译时,发现好多文件都被重新编译了。这种问题的发生,在于没有把“将接口从实现中分离”。Class的定义不只是详细叙述class接口,还包括许多实现细目:

class Person{public:Person(const std::string& name, const Date& birthday, const Address& addr);std::string name() const;std::string birthDate() const;std::string address() const;……private://实现细目std::string theName;Date the BirthDate;Address theAddress;};

要想编译,还要把class中用到的string、Date、Address包含进来。在Person定义文件的最前面,应该有:

#include<string>#include”date.h”#include”address.h”

这样一来,Person定义文件和其含入文件之间形成了一种编译依存关系(compilation dependency)。如果这些头文件有一个修改,那么使用Person class的文件要重新编译。这样的连串编译依存关系(cascading compliation dependencies)会给项目造成许多不便。 那么为什么C++把class的实现细目置于class定义式中?可以把实现细目分开:

namespace std{class string;//前置声明}Person{public:……};

首先不讨论前置声明是否正确(实际上是错误的),如果可以这么做,Person的客户只需要在Person接口被修改时才重新编译。但是这个想法有两个问题。 – 1、string不是个class,它是个typedef,定义为basic_string。上面对string的前者声明并不正确,正确的前置声明比较复杂,因为涉及额外的templates。实际上,我们不应该声明标准库,使用#include即可。标准头文件一般不会成为编译瓶颈,尤其是在你的建置环境中允许使用预编译头文件(precompiled headers)。如果解析(parsing)标准头文件是个问题,一般情况是你需要修改你的接口设计。 – 2、前置声明的每一件东西,编译器必须在编译期间知道对象的大小。例如

int main(){int x;Person p( params);……}

当编译器看到x定义式,必须知道给x分配多少内存;之后当编译器看到p的定义时,也应该知道必须给p分配多少内存。如果class的定义式不列出实现细目,编译器无法知道给p分配多少空间。

这个问题在Java等语言上不存在,因为它们在定义对象时,编译器只是分配一个指针(用来指向该对象)。上述代码实现是这个样子:

int main(){int x;Person* p;//定义一个指向Person的指针……}

在C++中,也可以这样做,将对象实现细目隐藏在一个指针背后。针对Person,可以把它分为两个classes,一个负责提供接口,另一个负责实现该接口。负责实现的接口取名为PersonImpl(Person implementation):

PersonImpl; //前置声明class Date;class Address;class Person{public:Person(const std::string& name, const Date& birthday, const Address& addr);std::string name() const;std::string birthDate() const;std::string address() const;……private://实现细目std::tr1::shared_ptr<PersonImpl> pImpl;//指针,指向实现};

这样的设计称为pimpl idiom(pimpl:pointer to implementation)。Person的客户和Date、Address以及Person的实现细目分离了。classes的任何实现修改都不要客户端重新编译。此外,客户还无法看到Person的实现细目,也就不会写出“取决于那些细目的代码”,真正实现了“接口与实现分离”。

这个分离的关键在于“声明的依存性”替换了“定义的依存性”,,这正是编译依存性最小化的本质:现实中,让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明(不是定义)相依。其他都源于以下设计策略:

<iosfwd>说明,本条款同样适用于templates和non-templates。条款 30中提到,template通常定义在头文件内,但也有些建置环境允许template定义在非头文件;这样就可以将“只含声明式”的头文件提供给templates。<iosfwd>就是这样一个文件。

C++中提供关键字export来将template声明和定义分割在不同文件内。但是支持export关键字的编译器并不多。

像Person这样使用pimpl idiom的classes叫做Handle classes。这样的class真正做事的方法之一是将他们所有的函数转交给相应的实现类(implementation classes),由实现类完成实际工作。例如Person的实现:

Personbirthday, const Address& addr):pImpl(new PersonImpl(name, birthday, addr)){}std::string Person::name() const{return pImpl->name();}……

在PersonImpl中,有着和Person完全相同的成员函数,两者接口完全相同。

还有一种实现Handle class的办法,那就是令Person成为一种特殊的abstract base class(抽象基类),称作Interface class。这样的class成员变量,只是描述derived classes接口(条款 34),也没有构造函数,只有一个virtual析构函数( 条款 7)和这一组pure virtual函数,用来叙述整个接口。

Interface classes类似Java和.NET的Interface,但是C++的Interface class不同于Java和.NET中的Interface,它允许有变量,更具有弹性。正如 条款 36所言,“non-virtual函数的实现”对继承体系内所有classes都应该相同。将这样的函数实现为Interface class(其中写有相应声明)的一部分也是合理的。

记忆的屏障,曾经心动的声音已渐渐远去。

《Effective C++》:条款31:将文件间的编译依存关系降至最低

相关文章:

你感兴趣的文章:

标签云: