Effective C++ 条款31
1 目的:如何真正的缩短编译时间?
写之前,关于Effective C++ 条款31你可以搜索到很多文章,现在我得换个方式叙述,抄书没有太大意义。
答:我相信能真正缩短编译时间的方法,只能从代码耦合方面做文章
。而不是一些奇淫巧计。因为我们经常遇到,工程修改一点,编译半天的尴尬处境(整个世界都被编译了一遍),当然,我们希望被编译(牵连)的文件尽可能减少(尽可能降低文件的耦合度)
1.1 如何降低文件的耦合度?
看不懂,没关系,我会继续解释
1. 运用Handle Class技术降低接口和实现的耦合性
2. 运用Interface Class技术降低接口和实现的耦合性
2 技术1:运用Handle Class技术降低接口和实现的耦合性
什么是Handle Class?
答:可以理解成自己啥都不做,把任务交给一个真实的类(通过指针的方式指向)
如下,一个简单的的案例
2.1 提出问题:耦合性的来源?
在正式分析之前,我们再复习下声明和定义
的区别
- 声明:向编译器声明,但是并不会分配内存空间,编译器只知道有这样一个东西
- 定义:分配内存空间,比如int,绝大部分编译器会分配4B字节空间,以便存下这个int。那遇到类呢?
这里明确给出答案
类占的内存=所有虚函数的vptr+所有非静态的成员变量,当然继承的话,要加上所有的基类
- 静态变量,专门存在全局区
- 成员函数(静态/非静态),函数当然在代码区咯
- 成员变量,计算方法是什么就算什么
- 成员函数,不可能在类中分配内存的
- 虚函数,很特殊,以后再分享吧。
备注:到底占用多少内存,也要看内存对齐方式的。
我选择一篇文档,可以学习下
想象这样一个场景,一个Person类里面包含
- 一个名字(string)
- 一个生日(Date)
- 一个家庭住址(Address)
常规想法,我们可能会提前包含两个类
(因为Person类中引用了Date和Address),如下
#include "date.h"
#include "address.h"
下面是简要的代码
#include <string>
#include "date.h"
#include "address.h"
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 m_Name;
Date m_BirthDate;
Address m_Address;
// 或许我会增加一些内容
};
一个很明显的问题:
如果定义一个Person对象,此时编译器会为我们分配内存,这是按照上面的“声明和定义
的区别”,编译器会查询m_Name、m_BirthDate、m_Address到底占用多少内存。很显然m_Name通过头文件string查找,而m_BirthDate、m_Address务必到对应的头文件查找,这就直接导致了文件的耦合。
很明显,这样写没有很大问题,但是,如果我之后还想添加一些成员变量呢?或者改变Date和Address呢?
- 使用该Person类的其他文件,需要重新编译
- 更极端的是,如果Date和Address被改变,将会导致雪崩式编译
2.2 解决问题:将Person分割成两个Class,一个负责提供接口,一个负责实现
这种想法很直接,其实还有一个在背后默默付出的类,叫做PersonImpl.h,它和 Person.h几乎一模一样。综上,这种解决方案,其实含有三个文件。
- Person.h只负责提供接口,自己啥都不做
- Person.cpp只负责实现
- PersonImpl.h,把任务交给一个真实的类
这里是 “Person.h”
- 浏览Person.h代码,你会发现就只有一个成员变量shared_ptr<PersonImpl>,想想上面复习的"
声明和定义
的区别",也就是Person类中只有一个指针,想想指针在内存中占多少呢?无疑,32位机器4B,64位机器8B - 很显然,指针大小是固定的,编译器不需要知道额外信息就能构建一个Person对象,如,32位机器这个类就只有4B
- 这就是接口类,一种采用pimpl idiom手法(pointer to implementation),指针通常称为m_pImpl。现在,Person类客户就完全和Data、Address、甚至和Person类的实现分离、实现真正意义上的接口和实现分离
你会不会怀疑,编译会报错?
答:肯定不会的,疑问?我都没有添加
#include "date.h"
#include "address.h"
然而,我却使用了
Person(const std::string& name, const Date& birthday, const Address& addr);
会不会报错?
如果你还有这个疑问,究其原因是因为你还有理解“声明和定义
的区别”,对于类,真正会分配内存的只有
类占的内存=所有虚函数的vptr+所有非静态的成员变量,当然继承的话,要加上所有的基类
如下,不管是函数是返回值,还是形参,都和内存没有半点联系,你只需要告诉编译器有这样一个东西即可,常见的手法就是使用前置声明
,这里涉及的三方类,都是前置声明。
class Date; // 前置声明
Date today(); //这些只是声明,绝不可能是定义,定义会分配内存
void clear(Date d); //这些只是声明,绝不可能是定义,定义会分配内存
// Person 接口--------------------------------记为"Person.h"
#include <string>
#include <memory>
// 前置声明
class PersonImpl; // Handle classes
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:
// pimpl idiom(pointer to implementation)
std::tr1::shared_ptr<PersonImpl> m_pImpl; // 指针,指向实现物,shared_ptr 在memory头文件
};
这里是 “Person.cpp”
下面是一个典型的模板,它主要实现了Person.h的四个函数,然而,它却调用了m_pImpl类的一些方法,而且连函数名都一模一样 ,究其原因是把任务交给一个真实的类PersonImpl,它和 Person几乎一模一样
// Person 实现--------------------------------记为"Person.cpp"
#include "Person.h"
#include "PersonImpl.h"
Person::Person(const std::string& name, const Date& birthday, const Address& addr)
: m_pImpl(new PersonImpl(name, birthday, addr))
{}
std::string Person::name() const {
return m_pImpl->name();
}
std::string birthDate() const{
return m_pImpl->birthDate();
}
std::string address() const{
return m_pImpl->address();
}
这里是 “PersonImpl.h”
这是真正工作的类PersonImpl,这里就会包含其他的头文件,显然,不管这个文件里卖怎么改写,都不会影响Person类!
// PersonImpl类(真实工作的类)--------------------------------记为"PersonImpl.h"
#include <string>
#include "date.h"
#include "address.h"
class PersonImpl {
public:
PersonImpl(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 m_Name;
Date m_BirthDate;
Address m_Address;
// 或许我会增加一些内容
};
3 技术2:运用Interface Class技术降低接口和实现的耦合性
回顾技术1的实现要点:自己啥都不做,把任务交给一个真实的类。
技术二的思想要点
实现一个接口Person类,而后真正做事的RealPerson类公共继承接口Person类
2.1 什么是Interface Class?
答:Interface Class就是所谓的接口类,也可以理解成抽象基类。含有以下特点
- 通常不含有成员变量
- 也没有构造函数
- 有一个虚的析构函数(virtual)
- 一组纯虚功能函数(pure virtual)
功能函数也被叫成描述/叙述整个接口,这种类叫做抽象基类(接口类)。同时,我们应当注意的是它没有构造函数,这种类绝不可定义对象,访问它的必须通过指针pointer或者引用reference
class Person {
public:
virtual ~Person(); // 一个虚析构函数
// 一组pure virtual功能函数
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
virtual std::string address() const = 0;
//...
};
2.2 如何为接口类创建对象?
从2.1中我们知道了接口类是不能创建对象的,但是这里有写了可以创建对象,注意,所谓的对象其实是pointer或者reference
那怎么实现这种操作呢?
答:这是一个trick,只需要在接口类中,写一个
static std::shared_ptr<Interface Class> create(xxx);
注意下面要点
- 遵守接口类返回pointer或者reference,用shared_ptr更加智能管理资源
- static,都会加上,加上static为类所有,而不是对象,所有对象共享
- 实现,放在下一小节,
改进后,如下
class Person {
public:
virtual ~Person(); // 一个虚析构函数
// 一组pure virtual功能函数
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
virtual std::string address() const = 0;
static std::shared_ptr<Person> create(const std::string& name, const Date& birthday, const Address& addr);
//...
};
2.3 实现一个接口Person类,而后真正做事的RealPerson类公共继承接口Person类
需要注意的是
- 接口类中的pure virtual需要被覆盖
- 接口类中的create
#include <string>
#include "date.h"
#include "address.h"
class RealPerson: public Person {
public:
RealPerson(const std::string& name, const Date& birthday, const Address& addr)
:theName(name), theBirthDate(birthday), theAddress(addr)
{}
virtual ~RealPerson() {}
// 重新覆盖接口类的pure virtual
std::string name() const;
std::string birthDate() const;
std::string address() const;
//...
private:
/*实现条目*/
std::string theName;
Date theBirthDate;
Address theAddress;
};
// 接口类中的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));
}
4 一些缺点
先看共同的缺点
- 这两种方式都无法实现inline,也就是编译器无法生成更高效的代码,确实有些可惜;
- 毫无疑问,virtual肯定是无法用inline的
4.1 Handle Class技术的缺点?
主要体现在
- 每一次访问对象的主体都是一次间接操作,势必会消耗多余的内存
4.2 Interface Class技术的缺点?
主要体现在
- 由于很多函数都是虚函数,无疑增加了很多vptr(包括继承),耗费更多的内存
- 虚函数在动态时候在知道真实的版本是啥,也相当于间接跳转
5 总结
再优秀的方法,总会有瑕疵,但是你不必顾及这些瑕疵,而放弃它带来的好处,还记得我们在解决什么问题吗?
降低个文件间的耦合性,这样可以加快编译速度
常用的两个手段是
- Handle Class:自己啥都不做,把任务交给一个真实的类(通过指针的方式指向)
- Interface Class:实现一个接口Person类,而后真正做事的RealPerson类公共继承接口Person类
大胆的使用这些方法。