Effective c++ 条款31:将文件间的编译依存关系降至最低

1、pimpl idiom,pointer to implementation

当我们编写一个class的实现,并在其中加入一些其他自定义或非内置类型的成员,一旦我们修改这些成员,整个程序都将重新编译。这是因为我们没有把“将接口从实现中分离”做的很好。
要改变这种现状,我们可以将实现细目分开描述,通过前置声明而非定义。
若我们的成员包含标准程序库的部分,我们仅应当使用适当的“#include”完成目的,而非简单的类似class Date;式的前置声明;且注意编译器需要知道该为class的某个对象分配多少内存,若class不列出实现细目,编译器又如何知道应该分配多少内存呢?
我们可以将对象实现细目隐藏于一个指针背后,也即pimpl。我们可以将class分割为两个classes,一个只提供接口,一个负责实现该接口。假设实现的接口声明为ClassNamePImpl,则在class中应该只包含一个ClassNamePImpl的智能指针。此时编译器便知道需要分配多少内存了。
这样的设计之下,class的客户就完全与实现细目分离,这样的分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。

  • 如果使用object references或object pointers可以完成任务,就不要使用objects。
    你可以只靠一个类型声明式就定义出指向该类型的引用和指针,但若干定义某类型的objects,就需要用到该类型的定义式。
  • 如果能够,尽量以class声明式替换class定义式。
  • 为声明式和定义式提供不同的头文件。
    为了促进严守上述准则,需要两个头文件,一个用于声明式,一个用于定义式。当然这些文件必须保持一致性。因此程序库客户应该总是#include一个声明文件而非前置声明若干函数,程序库作者也应该提供这两个头文件。

2、Handle classes和Interface classes

通常使用pimpl idiom的classes往往被称为Handle classes,它们将所有函数转交给相应的实现类并由后者完成实际工作,即调用实现类的函数。
另一个制作Handle classes的办法是,令class成为一种特殊的抽象基类,成为Interface class,这种class的目的是详细一一描述派生类的接口,因此它通常不带成员变量,也没有构造函数,只有一个virtual的析构函数以及一组pure virtual函数。但是c++并不阻止在interfaces内实现成员变量或成员函数,因为”non-virtual”函数的实现对继承体系内的所有classes都应该相同。
这个class的客户必须以class的指针和引用来撰写应用程序,因为它不可能针对内含纯虚函数的class具现出实体。就像Handle classes的客户一样,除非Interface classes的接口被修改否则其客户不需重新编译。
Interface classes的客户必须有办法为这种classes创建新对象,他们通常调用一个特殊函数,此函数扮演“真正将被具现化”的那个derived class的构造函数角色。这样的函数通常称为factory(工厂)函数或virtual构造函数(类似于我们平时希望virtual函数达成的目的,不是真的virtual构造函数)。它们返回指针(智能指针),指向动态分配所得对象。这样的函数又往往在Interface classes内被声明为static。
当然,支持Interface classes接口的那个具现类必须被定义出来,而且真正的构造函数必须被调用。一切都在virtual构造函数实现码所在的文件内秘密发生。

3、Handles和Interface classes的代价

Handles和Interface classes解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性。但是它们使得你在运行期丧失若干速度,又为你为每个对象超额付出若干内存。
在Handles classes身上,成员函数必须通过实现类指针取得对象数据,那会为每一次访问增加一层间接性,而每个对象消耗的内存数量必须增加实现类指针的大小。最后,实现类指针必须初始化,指向一个动态分配得来的实现类对象,所以将蒙受因动态分配带来的额外开销,以及遭遇bad_alloc异常的可能性。
Interface classes每个函数都是virtual,所以你必须为每次函数调用付出一个间接跳跃成本。此外Interface classes派生的对象必须内含一个vptr,这个指针可能会增加存放对象所需的内存数量——实际取决于这个对象除了Interface classes之外是否还有其他virtual函数来源。
然而,如果只是因为若干额外成本便不考虑Handles和Interface classes,将是严重的错误。你应该考虑以渐进方式使用这些技术。在程序开发过程中使用Handles和Interface classes以求实现码有所变化时对其客户带来最小冲击。而当他们导致速度和/或大小差异过于重大以至于classes之间的耦合相形之下不成为关键时,就以具象类替换Handles和Interface classes。

猜你喜欢

转载自blog.csdn.net/unirrrrr/article/details/81369076