zoukankan      html  css  js  c++  java
  • [Effective C++ --031]将文件间的编译依存关系降至最低

    引言:编译时间成本

    在项目中我们都会碰到修改既存类的情况:某个class实现文件做了些轻微改变,修改的不是接口,而是实现,而且只改private成分。

    重新build这个程序,并预计只花数秒就好,当按下“Build”,结果整个世界都被重新编译和链接了!

    问题是在c++并没有把“将接口从实现中分离”做得很好。class 的定义式不只详细叙述了class接口,还包括十足的实现细目:

    例如:

     1 class Person{ 
     2 public: 
     3     Person(const std::string& name, const Date& birthday, const Address& addr); 
     4     std::string name() const; 
     5     std::string birthDate() const; 
     6     std::string address() const; 
     7     ... 
     8 private: 
     9     std::string theName;        //实现细节 
    10     Date theBirthDate;          //实现细节
    11     Address theAddress;         //实现细节 
    12 };

    在这个类上方,应该还存在着:

    1 #include <string> 
    2 #include "date.h" 
    3 #include "address.h"

    头文件。

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

    第一节 实现细节和声明分开

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

     1 namespace std { class string;} //前置声明(不正确) 
     2 class Date;                    //前置声明 
     3 class Address;                 //前置声明 
     4 class Person{ 
     5 public: 
     6     Person(const std::string& name, const Date& birthday, const Address& addr); 
     7     std::string name() const; 
     8     std::string birthDate() const; 
     9     std::string address() const; 
    10     ... 
    11 };

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

    但这样有两个问题:

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

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

    1 int main() 
    2 { 
    3     int x;             // 定义一个int
    4     Person p(params);  // 定义一个Person
    5 }

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

    针对这个问题,smalltalk,java实现了一个类似下面代码的逻辑:

    1 int main() 
    2 { 
    3     int x;             // 定义一个int
    4     Person p(params);  // 定义一个指针指向Person
    5 }

    这样的功能在C++被叫做PIMPL,即:

     1 #include <string> 
     2 #include <memory> 
     3 class PersonImpl; 
     4 class Date; 
     5 class Address; 
     6 class Person{ 
     7 public: 
     8     Person(const std::string& name, const Date& birthday, const Address& addr); 
     9     std::string name()const; 
    10     std::string birthDate() const; 
    11     std::string address()const; 
    12     ... 
    13 private: 
    14     std::tr1::shared_ptr<PersonImpl> pImpl; //指向实现物的指针 
    15 };

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

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

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

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

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

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

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

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

    1 #include "Person.h" 
    2 #include "PersonImpl.h" 
    3 Person::Person(const std::string& name, const Date& birthday, const Address& addr) 
    4             : pImpl(new PersonImpl(name, birthday, addr)) 
    5 {} 
    6 std::string Person::name() const 
    7 { 
    8     return pImpl->name(); 
    9 }

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

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

    1 class Person{ 
    2 public: 
    3     virtual ~Person(); 
    4     virtual std::string name() const = 0; 
    5     virtual std::string birthday() const = 0; 
    6     virtual std::string address() const = 0; 
    7     ... 
    8 };

    ◆总结

    1.支持“编译依存最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes。

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

  • 相关阅读:
    【转】java对File.listFiles()排序
    java 获取当前目录文件名
    python批量创建文件夹
    [好课推荐]数据结构与算法python实现
    SCI论文重复率与降重
    [转]一图搞定Matplotlib
    [GitHub寻宝]机器学习实战python3代码分享
    [好课推荐]人工智能实践:Tensorflow2.0
    [转]用深度学习给黑白照片上色
    java split函数分割字符串
  • 原文地址:https://www.cnblogs.com/hustcser/p/4241458.html
Copyright © 2011-2022 走看看