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

Item 31: 最小化文件之间的 compilation dependencies(编译依赖)

作者:Scott Meyers

译者:fatalerror99 (iTePub's Nirvana)

发布:http://blog.csdn.net/fatalerror99/

你进入到你的 C++ 程序中,并对一个 class 的 implementation(实现)进行了细微的改变。提醒你一下,不是 class 的 interface(接口),只是 implementation(实现),仅仅是 private 的东西。然后你 rebuild(重建)这个程序,预计这个任务应该只花费几秒钟。毕竟只有一个 class 被改变。你点了一下 Build 或者键入 make(或者其它类似的事情),然后你惊呆了,继而被郁闷,就像你突然意识到整个世界都被重新编译和连接!当这样的事情发生的时候,你不讨厌它吗?

问题在于 C++ 没有做好从 implementations(实现)中剥离 interfaces(接口)的工作。一个 class definition(类定义)不仅指定了一个 class interface(类接口)而且有相当数量的 implementation details(实现细节)。例如:

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;        // implementation detail
      Date theBirthDate;          // implementation detail
      Address theAddress;         // implementation detail
};

在这里,如果不访问 Person 的 implementations(实现)使用到的 class,也就是 stringDateAddress 的定义,class Person 就无法编译。这样的定义一般通过 #include 指令提供,所以在定义 Person class 的文件中,你很可能会找到类似这样的东西:

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

不幸的是,这样就建立了定义 Person 的文件和这些头文件之间的 compilation dependency(编译依赖)。如果这些头文件中的任何一个发生了变化,或者如果这些头文件所依赖的任何一个文件发生了变化,包含 Person class 的文件和使用了 Person 的任何文件一样必须重新编译,这样的 cascading compilation dependencies(层叠编译依赖)导致了数不清的麻烦。

你也许想知道 C++ 为什么坚持要将一个 class 的 implementation details(实现细节)放在 class definition(类定义)中。例如,为什么你不能这样定义 Person,单独指定这个 class 的 implementation details(实现细节)呢?

namespace std {
     class string;             // forward declaration (an incorrect
}                              // one — see below)

class Date;                    // forward declaration
class Address;                 // forward declaration

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;
    ...
};

如果这样可行,只有在 class 的 interface 发生变化时,Person 的客户才有必要重新编译。

这个想法有两个问题。第一个,string 不是一个 class,它是一个 typedef (for basic_string<char>)。造成的结果就是,string 的 forward declaration(前向声明)是不正确的。正确的 forward declaration(前向声明)要复杂得多,因为它包括其它的模板。然而,这还不是要紧的,因为你不应该试图手动声明标准库的部件。替代做法是,直接使用适当的 #includes 并让它去做。标准头文件不太可能成为编译的瓶颈,特别是在你的构建环境允许你利用 precompiled headers(预编译头文件)时。如果解析标准头文件真的成为一个问题。你也许需要改变你的 interface 设计,避免使用导致不受欢迎的 #includes 的标准库部件。

第二个(而且更重要的)难点是 forward-declaring(前向声明)的每一件东西必须让编译器在编译期间知道它的 objects 的大小。考虑:

int main()
{
 int x;                // define an int

 Person p( params );   // define a Person
   ...
}

当编译器看到 x 的定义,它们知道它们必须分配足够的空间(一般是在栈上)用于保存一个 int。这没什么问题,每一个编译器都知道一个 int 有多大。当编译器看到 p 的定义,它们知道它们必须分配足够的空间给一个 Person,但是它们怎么推测出一个 Person object 有多大呢?它们得到这个信息的唯一方法是参考这个 class 的定义,但是如果一个省略了实现细节的 class definition(类定义)是合法的,编译器怎么知道该分配多少空间呢?

这个问题在诸如 Smalltalk 和 Java 这样的语言中就不会发生,因为,在这些语言中,定义一个 object 时,编译器仅需要分配足够的空间给一个指向一个 object 的 pointer。也就是说,它们处理上面的代码就像这些代码是这样写的:

int main()
{
  int x;               // define an int

  Person *p;           // define a pointer to a Person
  ...
}

这当然是合法的 C++,所以你也可以自己来玩这种“将 object 的实现隐藏在一个指针后面”的游戏。对 Person 做这件事的一种方法就是将它分开到两个 classes 中,其中一个仅提供一个 interface,另一个实现这个 interface。如果那个 implementation class(实现类)名为 PersonImplPerson 就可以如此定义:

#include <string>                      // standard library components
                                       // shouldn't be forward-declared

#include <memory>                      // for tr1::shared_ptr; see below

class PersonImpl;                      // forward decl of Person impl. class
class Date;                            // forward decls of classes used in

class Address;                         // Person interface
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:                                   // ptr to implementation;
  std::tr1::shared_ptr<PersonImpl> pImpl;  // see Item 13 for info on
};                                         // std::tr1::shared_ptr

在这里,main class (Person) 除了一个指向它的 implementation class(实现类) (PersonImpl) 的指针(这里是一个 tr1::shared_ptr ——参见 Item 13)之外不包含任何 data member。这样一个设计通常被说成是使用了 pimpl idiom ("pointer to implementation")(“指向实现的指针”)。在这样的 classes 中,那个指针的名字经常是 pImpl,就像上面那个。

用这样的设计,使 Person 的客户脱离 dates,addresses 和 persons 的细节。这些 classes 的实现可以随心所欲地改变,但 Person 的客户却不必重新编译。另外,因为他们看不到 Person 的实现细节,客户就不太可能写出以某种方式依赖那些细节的代码。这就是 interface(接口)和 implementation(实现)的真正分离。

这个分离的关键就是用对 declarations(声明)的依赖替代对 definitions(定义)的依赖。这就是 minimizing compilation dependencies(最小化编译依赖)的精髓:只要能实现,就让你的头文件独立自足,如果不能,就依赖其它文件中的声明,而不是定义。其它每一件事都从这个简单的设计策略产生。因此

(本篇未完,点击此处,接下篇)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值