C++ 如何缩短编译时间(Effective C++ 条款31:将文件间的编译依存关系降至最低)

1 目的:如何真正的缩短编译时间?

写之前,关于Effective C++ 条款31你可以搜索到很多文章,现在我得换个方式叙述,抄书没有太大意义。
答:我相信能真正缩短编译时间的方法,只能从代码耦合方面做文章。而不是一些奇淫巧计。因为我们经常遇到,工程修改一点,编译半天的尴尬处境(整个世界都被编译了一遍),当然,我们希望被编译(牵连)的文件尽可能减少(尽可能降低文件的耦合度)

1.1 如何降低文件的耦合度?

看不懂,没关系,我会继续解释
1. 运用Handle Class技术降低接口和实现的耦合性
2. 运用Interface Class技术降低接口和实现的耦合性

2 技术1:运用Handle Class技术降低接口和实现的耦合性

什么是Handle Class?
答:可以理解成自己啥都不做,把任务交给一个真实的类(通过指针的方式指向)

如下,一个简单的的案例

2.1 提出问题:耦合性的来源?

在正式分析之前,我们再复习下声明和定义的区别

  1. 声明:向编译器声明,但是并不会分配内存空间,编译器只知道有这样一个东西
  2. 定义:分配内存空间,比如int,绝大部分编译器会分配4B字节空间,以便存下这个int。那遇到类呢?

这里明确给出答案
类占的内存=所有虚函数的vptr+所有非静态的成员变量,当然继承的话,要加上所有的基类

  1. 静态变量,专门存在全局区
  2. 成员函数(静态/非静态),函数当然在代码区咯
  3. 成员变量,计算方法是什么就算什么
  4. 成员函数,不可能在类中分配内存的
  5. 虚函数,很特殊,以后再分享吧。

备注:到底占用多少内存,也要看内存对齐方式的。
我选择一篇文档,可以学习下

想象这样一个场景,一个Person类里面包含

  1. 一个名字(string)
  2. 一个生日(Date)
  3. 一个家庭住址(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呢?

  1. 使用该Person类的其他文件,需要重新编译
  2. 更极端的是,如果Date和Address被改变,将会导致雪崩式编译

2.2 解决问题:将Person分割成两个Class,一个负责提供接口,一个负责实现

这种想法很直接,其实还有一个在背后默默付出的类,叫做PersonImpl.h,它和 Person.h几乎一模一样。综上,这种解决方案,其实含有三个文件。

  1. Person.h只负责提供接口,自己啥都不做
  2. Person.cpp只负责实现
  3. PersonImpl.h,把任务交给一个真实的类

这里是 “Person.h”

  1. 浏览Person.h代码,你会发现就只有一个成员变量shared_ptr<PersonImpl>,想想上面复习的"声明和定义的区别",也就是Person类中只有一个指针,想想指针在内存中占多少呢?无疑,32位机器4B,64位机器8B
  2. 很显然,指针大小是固定的,编译器不需要知道额外信息就能构建一个Person对象,如,32位机器这个类就只有4B
  3. 这就是接口类,一种采用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就是所谓的接口类,也可以理解成抽象基类。含有以下特点

  1. 通常不含有成员变量
  2. 也没有构造函数
  3. 有一个虚的析构函数(virtual)
  4. 一组纯虚功能函数(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);

注意下面要点

  1. 遵守接口类返回pointer或者reference,用shared_ptr更加智能管理资源
  2. static,都会加上,加上static为类所有,而不是对象,所有对象共享
  3. 实现,放在下一小节,

改进后,如下

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类

需要注意的是

  1. 接口类中的pure virtual需要被覆盖
  2. 接口类中的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 一些缺点

先看共同的缺点

  1. 这两种方式都无法实现inline,也就是编译器无法生成更高效的代码,确实有些可惜;
  2. 毫无疑问,virtual肯定是无法用inline的

4.1 Handle Class技术的缺点?

主要体现在

  1. 每一次访问对象的主体都是一次间接操作,势必会消耗多余的内存

4.2 Interface Class技术的缺点?

主要体现在

  1. 由于很多函数都是虚函数,无疑增加了很多vptr(包括继承),耗费更多的内存
  2. 虚函数在动态时候在知道真实的版本是啥,也相当于间接跳转

5 总结

再优秀的方法,总会有瑕疵,但是你不必顾及这些瑕疵,而放弃它带来的好处,还记得我们在解决什么问题吗?
降低个文件间的耦合性,这样可以加快编译速度
常用的两个手段是

  1. Handle Class:自己啥都不做,把任务交给一个真实的类(通过指针的方式指向)
  2. Interface Class:实现一个接口Person类,而后真正做事的RealPerson类公共继承接口Person类

大胆的使用这些方法。

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值