[Effective C++ --031]将文件间的编译依存关系降至最低
引言:编译时间成本
在项目中我们都会碰到修改既存类的情况:某个class实现文件做了些轻微改变,修改的不是接口,而是实现,而且只改private成分。
重新build这个程序,并预计只花数秒就好,当按下“Build”,结果整个世界都被重新编译和链接了!
问题是在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; //实现细节
};
在这个类上方,应该还存在着:
#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; // 定义一个int
Person p(params); // 定义一个Person
}
编译器看到X的时候,它都知道一个int有多大。但是当它看到p的时候,知道必须分配足够空间放置一个Person,但是他必须知道一个Person对象多大,获得这一信息的唯一办法是询问class定义式。然而,如果class定义式可以合法的不列出实现细目,编译器如何知道该分配多少空间?
针对这个问题,smalltalk,java实现了一个类似下面代码的逻辑:
int main()
{
int x; // 定义一个int
Person p(params); // 定义一个指针指向Person
}
这样的功能在C++被叫做PIMPL,即:
#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的客户就完全与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);
为声明式和定义式提供不同的头文件。
这种使用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 = ;
virtual std::string birthday() const = ;
virtual std::string address() const = ;
...
};
◆总结
1.支持“编译依存最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes。
2.程序库头文件应该以“安全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。
[Effective C++ --031]将文件间的编译依存关系降至最低的更多相关文章
- 读书笔记_Effective_C++_条款三十一:将文件间的编译依存关系降至最低(第三部分)
下面来谈谈书中的第二部分,用Interface Classes来降低编译的依赖.从上面也可以看出,避免重编的诀窍就是保持头文件(接口)不变化,而保持接口不变化的诀窍就是不在里面声明编译器需要知道大小的 ...
- Effective C++ -----条款31:将文件间的编译依存关系降至最低
支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式.基于此构想的两个手段是Handle classes 和 Interface classes. 程序库头文件应该以“完全且仅有声明式 ...
- 条款31:将文件间的编译依存关系降至最低(Minimize compilation dependencies between files)
NOTE1: 1.支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式.基于此构想的两个手段是Handle classes 和 Interface classes. 2.程序库头文件应 ...
- [EffectiveC++]item31:将文件间的编译依存关系降至最低
P143:“声明的依赖性"替换“定义的依存性”
- effective c++:inline函数,文件间编译依存关系
inline函数 inline函数可以不受函数调用所带来的额外开销,编译器也会优化这些不含函数调用的代码,但是我们不能滥用Inline函数,如果程序中的每个函数都替换为inline函数那么生成的目标文 ...
- Effective C++ 笔记:条款 31 将编译关系降至最低
31 : Minimize compilation dependencies between files 1 这关乎C++的类(或说都是类惹的祸) 1.1 C++类定义式的问题 C++类定义式不只叙述 ...
- 读书笔记 effective c++ Item 31 把文件之间的编译依赖降到最低
1. 牵一发而动全身 现在开始进入你的C++程序,你对你的类实现做了一个很小的改动.注意,不是接口,只是实现:一个私有的stuff.然后你需要rebuild你的程序,计算着这个build应该几秒钟就足 ...
- 使用.bat 文件,批量编译项目文件。
使用.bat 文件,批量编译项目文件. 2008-6-1来源:www.aspcool.com 作者:PCJIM 点击:次 path %path%;D:\Program Files\Microsof ...
- c++中,保证头文件只被编译一次,避免多重包含的方法
保证头文件只被编译一次 #pragma once这是一个比较常用的C/C++杂注,只要在头文件的最开始加入这条杂注,就能够保证头文件只被编译一次. #pragma once是编译器相关的,有的编译器支 ...
随机推荐
- iOS已发布应用中对异常信息捕获和处理
iOS已发布应用中对异常信息捕获和处理 iOS开发中我们会遇到程序抛出异常退出的情况,如果是在调试的过程中,异常的信息是一目了然,但是如果是在已经发布的程序中,获取异常的信息有时候是比较困难的. iO ...
- 使用JSP处理用户注册和登陆
1. 这是一个JSP实例,由四个JSP页面组成,处理用户的注册和登陆信息2. 首先是login.jsp,代码如下:<html><center><form method=g ...
- netty的入门
netty是什么? netty是一个基于NIO的通信框架,对于传统计算机,系统的瓶颈一直在输入输出设备上,计算速度超过IO速度,所以对于i o的性能提高异常重要. 什么是NIO? 非阻塞IO,N表示n ...
- 求职基础复习之快速排序c++版
#include<iostream> using namespace std; int partition(int a[],int p,int q){ int x = a[q]; ; fo ...
- [Hive - Tutorial] Data Units 数据存储单位
Data Units In the order of granularity - Hive data is organized into: 数据库.表.分区.桶 Databases: Namespac ...
- 虚拟化技术对比:Xen vs KVM
恒天云:http://www.hengtianyun.com/download-show-id-68.html 一.说明 本文主要从功能方面和性能方面对Xen和KVM对比分析,分析出其优缺点指导我们恒 ...
- hdu 2063 过山车(匈牙利算法模板)
http://acm.hdu.edu.cn/showproblem.php?pid=2063 过山车 Time Limit: 1000/1000 MS (Java/Others) Memory ...
- [iOS基础控件 - 6.11.5] 沙盒 & 数据存储
A.沙盒 每个APP都有一个沙盒,是独立存在的 1.Xcode5和Xcode6的模拟器文件目录 a.模拟器路径改版 (1)Xcode5中模拟器路径为:/Users/用户名/Library/Appl ...
- srcelement、parentElement
srcElement 是Dom事件中的事件最初指派到的元素. 比如有一个div,里面有一个按钮.你响应div的onclick事件,但实际上,你单击的只是它内部的按钮,那么,srcElement指向的, ...
- HDU 5754 Life Winner Bo (找规律and博弈)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=5754 给你四种棋子,棋子一开始在(1,1)点,两个人B和G轮流按每种棋子的规则挪动棋子,棋子只能往右下 ...