Effective-cpp-#31

Minimize compilation dependencies between files

动机

这个问题主要是在多文件的程序中,如果某个class实现文件做了些许的修改,当你重新build/make这个项目时,可能所有相关依赖的文件都重新编译和链接了。这个就是“接口和实现没有很好分离的一大弊端”。

考虑这样一个程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
//可能包含的头文件
#include <string>
#include "date.h"
#include "address.h"

class Person{
public:
...
private:
std::string theName;
Date theBirthDate;
Address theAddress;
}

这里的问题是:在private中的那几个变量定义,我们需要找到Date和Address的具体定义,也就是说一旦这个文件依赖的头文件发生了变化,那么Person class也要重新编译。

解决方法

当然,我们可以分离声明和定义,比如在刚刚那个文件中声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Date;
class Address;

class Person{
public:
...
private:
std::string theName;
Date theBirthDate;
Address theAddress;
}

//另外的文件
int main()
{
int x;
Person p(params);
}

但这里的问题是,对于前置声明,编译器必须提前知道对象的大小,这样才能在stack中分配足够大的空间。

借鉴Java的经验,我们用一个指针去指向需要的对象。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <string>//一般来说,标准库组件不用前置声明
#include <memory>

class PersonImpl; //Person实现类的前置声明
class Date;
class Address;

class Person{
public:
...
private:
std::trl::shared_ptr<PersonImpl> pImpl;
}

这种设计就叫做——pointer to implementation。至此,Person的客户端就与其他class包括Person的实现细节分离了,也就是其他类的修改不会影响到Person客户端重新编译。

制作一个handle class

通常来说,我们应该为声明式和定义式提供不同的头文件,这样就不用每次都前置声明其他的class。

制作一个handle class的方法有两种,第一种已经在前面提及过了,至于第二种就是构造一个抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
class Person{
public:
virtual ~Person();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
virtual std::string address() const = 0;
...
static std::trl::shared_ptr<Person>
create(const std::string& name,
const Date& birthday,
const Address& addr);
}

当然,由于这是abstract class,我们需要一个concrete class来定义所有的函数。

建议

  • 为了保证编译依赖最小化,我们应该依赖于声明而不是定义。这里有两个实现手段——handle class和interface class;
  • 程序库头文件应该以“完整的并且是仅有声明”的形式存在;