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

假设你对c++程序的某个class实现文件做了些轻微改变,修改的不是接口,而是实现,而且只改private成分。

然后重新建置这个程序,并预计只花数秒就好,当按下“Build”或键入make,会大吃一惊,因为你意识到整个世界都被重新编译和链接了!

问题是在c++并没有把“将接口从实现中分离”做得很好。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 theBirthDate;          //实现细目
    Address theAddress;         //实现细目
};

这个class Person无法通过编译,Person定义文件的最上方可能存在这样的东西:

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

这样一来,便在Person定义文件和其含入文件之间形成了一种编译依存关系(compilation dependency)。如果这些头文件中有任何一个被改变,或这些文件所依赖的其他头文件有任何改变,那么每个含入Person class的文件就得重新编译,任何使用Person class的文件也必须重新编译。这样的的连串编译依存关系(cascading compilation dependencies)会对许多项目造成难以形容的灾难。

为什么c++坚持将class的实现细目置于class定义式中?为什么不这样定义Person,将实现细目分开叙述:

namespace std { class string;} //前置声明(不正确)
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;
    ...
};

如果这样,Person的客户就只有在Person接口被修改时才重新编译。

两个问题:第一,string不是个class,它是个typedef。因此string前置声明并不正确,而且你本来就不应该尝试手工声明一部分标准程序库。你应该仅仅使用适当的#includes完成目的。标准头文件不太可能成为编译瓶颈,

第二,编译器必须在编译期间知道对象的大小:

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

编译器知道必须分配足够空间放置一个Person,但是他必须知道一个Person对象多大,获得这一信息的唯一办法是询问class定义式。然而,如果class定义式可以合法的不列出实现细目,编译器如何知道该分配多少空间?

此问题在smalltalk,java等语言上并不存在,因为当我们以那种语言定义对象时,编译器只分配足够空间给一个指针(用于指向该对象)使用。就是说它们将上述代码视同这样子:

int main()
{
    int x;
    Person* p;
}

这当然也是合法的c++代码,所以你可以玩玩“将对象实现细目隐藏在一个指针背后”的游戏。可以把Person分割为两个classes,一个提供接口,另一个负责实现接口。负责实现的那个所谓的implementation class取名为PersonImpl,Person将定义如下:

#include <string>
#include <memory>
class 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; //指向实现物的指针
};

这里,Person只内含一个指针成员,指向其实现类(PersonImpl)。这个设计常被称为pimpl idiom(pimpl是“pointer to implementation”的缩写)。

这样,Person的客户就完全与Date,Address以及Person的实现细目分离了。那些classes的任何实现修改都不需要Person客户端重新编译。

这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译依存性最小化的本质:让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。其他每件事都源自于这个简单的涉及策略。

如果用object reference 或 object pointer可以完成任务,就不要用objects

可以只靠声明式定义出指向该类型的pointer和reference;但如果定义某类型的objects,就需要用到该类型的定义式。

如果能够,尽量以class声明式替换class定义式

当你声明一个函数而它用到某个class时,你并不需要该class的定义式,纵使函数以by value方式传递该类型的参数(或返回值)亦然:

class Date; //class 声明式
Date today();
void clearAppiontments(Date d);

声明today函数和clearAppointments函数无需定义Date,但是一旦有任何人调用那些函数,调用之前Date定义式一定得先曝光才行。如果能够将“提供class定义式”(通过#include完成)的义务从“函数声明所在”之头文件移转到“内含函数调用”之客户文件,便可将“并非真正必要之类型定义”与客户端之间的编译依存性去除掉。

为声明式和定义式提供不同的头文件

因此程序库客户应该总是#inlcude一个声明文件而非前置声明若干函数,

#include "datefwd,h" //这个头文件内声明class Date
Date today();
void clearAppointments(Date d);

只含声明式的那个头文件名为“datefwd.h”,像标准程序库的头文件“<iosfwd>”。他分外彰显“本条款适用于templates也适用于non-templates”。许多建置环境中template定义式同常被置于头文件中,但也有某些建置环境允许tamplates定义式放在“非头文件中”,这样就可以将“只含声明式”的头文件提供给templates。

这种使用pimpl idiom的classes,往往被称为Handle classes。

这种classes的办法之一就是将他们的所有函数转交给相应的实现类(implementation classes)并由后者完成实际工作。

#include "Person.h"
#include "PersonImpl.h"
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();
}

另一个制作Handle class的办法是,令Person称为一种特殊的abstract base class(抽象基类)称为Interface classes。这种class的目的是详细一一描述derived classes的接口,因此它通常不带成员变量,也没有构造函数,只有一个virtual析构函数以及一组pure virtual函数,又来叙述整个接口。

一个针对Person而写的Interface class或许看起来像这样:

class Person{
public:
    virtual ~Person();
    virtual std::string name() const = 0;
    virtual std::string birthday() const = 0;
    virtual std::string address() const = 0;
    ...
};

这个class的客户必须以Person的pointers和reference来撰写应用程序,不能针对“内含pure virtual函数”的Person classes具现出实体。除非Interface class的接口被修改否则其客户不需要重新编译。

Interface class 的客户必须有办法为这种class创建新对象。它们通常调用一个特殊函数,此函数扮演一个“真正将被具现化”的那个derived class的构造函数角色。通常称为工厂factory函数或virtual 构造函数。它们返回指针,指向动态分配所得对象,而该对象支持interface class的接口。这样的函数又往往在interface class内被声明为static:

class Person{
public:
    ...
    static std::tr1::shared_ptr<Person>
    create(const std::string& name, const Date& birthday, const Address& addr);
};

客户可能会这样使用它们:

std::string name;
Date dateBirth;
Address address;
std::tr1::shared_ptr<Person> pp(Person::create(name, dateBirth, address));
...
std::cout << pp->name()
            << "was born on "
            << PP->birthDate()
            << " and now lives at "
            << pp->address();
...

当然支持interface class接口的那个具象类(concrete classes)必须被定义出来,而真正的构造函数必须被调用。

假设有个derived class RealPerson,提供继承而来的virtual函数的实现:

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;
    std::string birthDate() const;
    std::string address() const;
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对象,取决于诸如额外参数值、独自文件或数据库的数据、环境变量等等。

RealPerson示范实现了Interface class的两个最常见机制之一:从interface class继承接口规格,然后实现出接口所覆盖的函数。

handle classes 和 interface classes解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性。

handle classed身上,成员函数必须通过implementation pointer取得对象数据。那会为每一次访问增加一层间接性。每个对象消耗的内存必须增加一个implementation pointer的大小。implementation pointer必须初始化指向一个动态分配的implementation object,所以还得蒙受因动态内存分配儿带来的额外开销。

Interface classes,由于每个函数都是virtual,必须为每次函数调用付出一个间接跳跃。此外Interface class派生的对象必须内含一个vptr(virtual table pointer)。

在程序开发过程中使用handle class 和 interface class以求实现码有所改变时对其客户带来最小冲击。

而当他们导致速度和/或大小差异过于重大以至于class之间的耦合相形之下不成为关键时,就以具象类(concrete class)替换handle class 和 interface class。

支持“编译依存最小化”的一般构想是:相依于声明式,不要相依于定义式。

程序库头文件应该以“安全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。

posted @ 2012-02-05 22:51  lidan  阅读(1023)  评论(0编辑  收藏  举报