《Effective C++》:条款31:将文件间的编译依存关系降至最低
2015-02-23 23:07
627 查看
假如你在修改程序,只是修改了某个class的接口的实现,而且修改的是private部分。之后,你编译时,发现好多文件都被重新编译了。这种问题的发生,在于没有把“将接口从实现中分离”。Class的定义不只是详细叙述class接口,还包括许多实现细目:
要想编译,还要把class中用到的string、Date、Address包含进来。在
这样一来,
那么为什么C++把class的实现细目置于class定义式中?可以把实现细目分开:
首先不讨论前置声明是否正确(实际上是错误的),如果可以这么做,Person的客户只需要在Person接口被修改时才重新编译。但是这个想法有两个问题。
- 1、string不是个class,它是个typedef,定义为basic_string。上面对string的前者声明并不正确,正确的前置声明比较复杂,因为涉及额外的templates。实际上,我们不应该声明标准库,使用#include即可。标准头文件一般不会成为编译瓶颈,尤其是在你的建置环境中允许使用预编译头文件(precompiled headers)。如果解析(parsing)标准头文件是个问题,一般情况是你需要修改你的接口设计。
- 2、前置声明的每一件东西,编译器必须在编译期间知道对象的大小。例如
当编译器看到x定义式,必须知道给x分配多少内存;之后当编译器看到p的定义时,也应该知道必须给p分配多少内存。如果class的定义式不列出实现细目,编译器无法知道给p分配多少空间。
这个问题在Java等语言上不存在,因为它们在定义对象时,编译器只是分配一个指针(用来指向该对象)。上述代码实现是这个样子:
在C++中,也可以这样做,将对象实现细目隐藏在一个指针背后。针对Person,可以把它分为两个classes,一个负责提供接口,另一个负责实现该接口。负责实现的接口取名为PersonImpl(Person implementation):
这样的设计称为pimpl idiom(pimpl:pointer to implementation)。Person的客户和Date、Address以及Person的实现细目分离了。classes的任何实现修改都不要客户端重新编译。此外,客户还无法看到Person的实现细目,也就不会写出“取决于那些细目的代码”,真正实现了“接口与实现分离”。
这个分离的关键在于“声明的依存性”替换了“定义的依存性”,这正是编译依存性最小化的本质:现实中,让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明(不是定义)相依。其他都源于以下设计策略:
如果使用object references或object pointers可以完成任务,就不要使用object。因为,使用references或pointers只需要一个声明,而定义objects需要使用该类型的定义。
如果可以,尽量以class声明式替换class定义式。但是,当声明函数使用某个class时,即使是by value方式传递该类型参数/返回值,都不需要class定义。但是在使用这些函数时,这些classes在调用函数前一定要先曝光。客户终究是要知道classes的定义式,但是这样做的意义在于:将提供class定义式(通过#include完成)的义务,从函数声明所在头文件,转移到函数调用的客户文件。
为声明式和定义式提供两个不同的头文件。程序中,不应该让客户给出前置声明,程序作者一般提供两个头文件,一个用于声明式,一个用于定义式。在C++标准库的头文件中(条款54),
C++中提供关键字export来将template声明和定义分割在不同文件内。但是支持export关键字的编译器并不多。
像Person这样使用pimpl idiom的classes叫做Handle classes。这样的class真正做事的方法之一是将他们所有的函数转交给相应的实现类(implementation classes),由实现类完成实际工作。例如Person的实现:
在PersonImpl中,有着和Person完全相同的成员函数,两者接口完全相同。
还有一种实现Handle class的办法,那就是令Person成为一种特殊的abstract base class(抽象基类),称作Interface class。这样的class成员变量,只是描述derived classes接口(条款 34),也没有构造函数,只有一个virtual析构函数( 条款 7)和这一组pure virtual函数,用来叙述整个接口。
Interface classes类似Java和.NET的Interface,但是C++的Interface class不同于Java和.NET中的Interface,它允许有变量,更具有弹性。正如 条款 36所言,“non-virtual函数的实现”对继承体系内所有classes都应该相同。将这样的函数实现为Interface class(其中写有相应声明)的一部分也是合理的。
像Person这样的Interface class可以这些写:
这个class的客户必须使用Person的pointers或references,因为内含pure virtual函数的class无法实例化。这样一来,只要Interface class的接口不被修改,其他客户就不需要重新编译。
Interface class的客户在为class创建新对象时,通常使用一个特殊函数,这个函数扮演“真正将被具体化”的那个derived classes的构造函数的角色。这样的函数叫做工程函数factory(条款13)或virtual构造函数,它们返回指针(更有可能为智能指针,**条款**18),指向动态分配所得对象,这个对象支持Interface class接口。factory函数通常声明为static
客户这样使用
支持Interface class接口的那个具体类(concrete classes)在真正的构造函数调用之前要被定义好。例如,有个RealPerson继承了Person
有了RealPerson之后,就可以写Person::create了
RealPerson实现Interface class的机制是:从Interface class继承接口,然后实现出接口所覆盖的函数。还有一种实现方法,设计多重继承,在**条款**40探讨。
Handle classes和Interface classes解除了接口和实现之间的耦合关系,从而降低了编译依存性。但是为了也带了一些代价:使你丧失了运行期间若干速度,又开辟了超出对象若干内存。
在Handle classes身上,成员函数通过implementation pointer取得对象数据。这样为访问增加了一层间接性,内存也增加了implementation pointer的大小。implementation pointer的初始化,还要带了动态开辟内存的额外开销,蒙受遭遇bad_alloc异常的可能性。
在Interface classes身上,每个函数都是virtual的,所以每次调用要付出一个间接跳跃(indirect jump)成本。其派生对象会有一个vptr(virtual table pointer,**条款**7),增加了对象所需内存。
Handle classes和Interface classes,一旦脱离inline函数,都无法有太大作为。**条款**30说明为什么inline函数要置于头文件,但Handle classes和Interface classes被设计用来隐藏实现细节。
我们要做的是,在程序中使用Handle classes和Interface classes,以求实现代码有所变化时,对其客户带来最小影响。但如果它们导致的额外成本过大,例如导致运行速度或对象大小差异过大,以至于classes之间的耦合相比之下不成为关键时,就以具体类(concrete classes)替换Handle classes和Interface classes。
总结
- 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义时。基于此构想的两个手段是Handle classes和Interface classes。
- 程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。不论是否这几templates,这种做法都是适用。
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 the BirthDate; Address theAddress; };
要想编译,还要把class中用到的string、Date、Address包含进来。在
Person定义文件的最前面,应该有:
#include<string> #include"date.h" #include"address.h"
这样一来,
Person定义文件和其含入文件之间形成了一种编译依存关系(compilation dependency)。如果这些头文件有一个修改,那么使用Person class的文件要重新编译。这样的连串编译依存关系(cascading compliation dependencies)会给项目造成许多不便。
那么为什么C++把class的实现细目置于class定义式中?可以把实现细目分开:
namespace std{ class string;//前置声明 } class Date;//前置声明 class Address;//前置声明 class Person{ public: …… };
首先不讨论前置声明是否正确(实际上是错误的),如果可以这么做,Person的客户只需要在Person接口被修改时才重新编译。但是这个想法有两个问题。
- 1、string不是个class,它是个typedef,定义为basic_string。上面对string的前者声明并不正确,正确的前置声明比较复杂,因为涉及额外的templates。实际上,我们不应该声明标准库,使用#include即可。标准头文件一般不会成为编译瓶颈,尤其是在你的建置环境中允许使用预编译头文件(precompiled headers)。如果解析(parsing)标准头文件是个问题,一般情况是你需要修改你的接口设计。
- 2、前置声明的每一件东西,编译器必须在编译期间知道对象的大小。例如
int main() { int x; Person p( params); …… }
当编译器看到x定义式,必须知道给x分配多少内存;之后当编译器看到p的定义时,也应该知道必须给p分配多少内存。如果class的定义式不列出实现细目,编译器无法知道给p分配多少空间。
这个问题在Java等语言上不存在,因为它们在定义对象时,编译器只是分配一个指针(用来指向该对象)。上述代码实现是这个样子:
int main() { int x; Person* p;//定义一个指向Person的指针 …… }
在C++中,也可以这样做,将对象实现细目隐藏在一个指针背后。针对Person,可以把它分为两个classes,一个负责提供接口,另一个负责实现该接口。负责实现的接口取名为PersonImpl(Person implementation):
#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;//指针,指向实现 };
这样的设计称为pimpl idiom(pimpl:pointer to implementation)。Person的客户和Date、Address以及Person的实现细目分离了。classes的任何实现修改都不要客户端重新编译。此外,客户还无法看到Person的实现细目,也就不会写出“取决于那些细目的代码”,真正实现了“接口与实现分离”。
这个分离的关键在于“声明的依存性”替换了“定义的依存性”,这正是编译依存性最小化的本质:现实中,让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明(不是定义)相依。其他都源于以下设计策略:
如果使用object references或object pointers可以完成任务,就不要使用object。因为,使用references或pointers只需要一个声明,而定义objects需要使用该类型的定义。
如果可以,尽量以class声明式替换class定义式。但是,当声明函数使用某个class时,即使是by value方式传递该类型参数/返回值,都不需要class定义。但是在使用这些函数时,这些classes在调用函数前一定要先曝光。客户终究是要知道classes的定义式,但是这样做的意义在于:将提供class定义式(通过#include完成)的义务,从函数声明所在头文件,转移到函数调用的客户文件。
为声明式和定义式提供两个不同的头文件。程序中,不应该让客户给出前置声明,程序作者一般提供两个头文件,一个用于声明式,一个用于定义式。在C++标准库的头文件中(条款54),
<iosfwd>内含iostream各组件的声明式,其对应定义分布在不同文件件,包括
<sstream>,
<streambuf>,
<fstream>,
<iostream>。
<iosfwd>说明,本条款同样适用于templates和non-templates。条款 30中提到,template通常定义在头文件内,但也有些建置环境允许template定义在非头文件;这样就可以将“只含声明式”的头文件提供给templates。
<iosfwd>就是这样一个文件。
C++中提供关键字export来将template声明和定义分割在不同文件内。但是支持export关键字的编译器并不多。
像Person这样使用pimpl idiom的classes叫做Handle classes。这样的class真正做事的方法之一是将他们所有的函数转交给相应的实现类(implementation classes),由实现类完成实际工作。例如Person的实现:
#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(); } ……
在PersonImpl中,有着和Person完全相同的成员函数,两者接口完全相同。
还有一种实现Handle class的办法,那就是令Person成为一种特殊的abstract base class(抽象基类),称作Interface class。这样的class成员变量,只是描述derived classes接口(条款 34),也没有构造函数,只有一个virtual析构函数( 条款 7)和这一组pure virtual函数,用来叙述整个接口。
Interface classes类似Java和.NET的Interface,但是C++的Interface class不同于Java和.NET中的Interface,它允许有变量,更具有弹性。正如 条款 36所言,“non-virtual函数的实现”对继承体系内所有classes都应该相同。将这样的函数实现为Interface class(其中写有相应声明)的一部分也是合理的。
像Person这样的Interface class可以这些写:
class Person{ public: virtual ~Person(); virtual std::string name() const=0; virtual std::string birthDate()const=0; virtual std::string address()const=0; …… };
这个class的客户必须使用Person的pointers或references,因为内含pure virtual函数的class无法实例化。这样一来,只要Interface class的接口不被修改,其他客户就不需要重新编译。
Interface class的客户在为class创建新对象时,通常使用一个特殊函数,这个函数扮演“真正将被具体化”的那个derived classes的构造函数的角色。这样的函数叫做工程函数factory(条款13)或virtual构造函数,它们返回指针(更有可能为智能指针,**条款**18),指向动态分配所得对象,这个对象支持Interface class接口。factory函数通常声明为static
class Person{ public: …… static std::tr1::shared_ptr<Person create(……) …… };
客户这样使用
std::string name; Date dateOfBirth; Address address; std::tr1::shared_ptr<Person> pp(Person::create(……)); std::cout<<pp->name()<<"was born"<<pp->birthDate()<<"and now lives at"<<pp->address();
支持Interface class接口的那个具体类(concrete classes)在真正的构造函数调用之前要被定义好。例如,有个RealPerson继承了Person
class RealPerson: public Person{ public: RealPerson(const std::string& name, const Date& birthday, const Address& addr) :theName(name), theBirthDate(birthday), theAddress(addr) {} virtual ~RealPerson(){}; std::string name() const; …… private: std::string theName; Date theBirthDate; Address theAddress; };
有了RealPerson之后,就可以写Person::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)); }
RealPerson实现Interface class的机制是:从Interface class继承接口,然后实现出接口所覆盖的函数。还有一种实现方法,设计多重继承,在**条款**40探讨。
Handle classes和Interface classes解除了接口和实现之间的耦合关系,从而降低了编译依存性。但是为了也带了一些代价:使你丧失了运行期间若干速度,又开辟了超出对象若干内存。
在Handle classes身上,成员函数通过implementation pointer取得对象数据。这样为访问增加了一层间接性,内存也增加了implementation pointer的大小。implementation pointer的初始化,还要带了动态开辟内存的额外开销,蒙受遭遇bad_alloc异常的可能性。
在Interface classes身上,每个函数都是virtual的,所以每次调用要付出一个间接跳跃(indirect jump)成本。其派生对象会有一个vptr(virtual table pointer,**条款**7),增加了对象所需内存。
Handle classes和Interface classes,一旦脱离inline函数,都无法有太大作为。**条款**30说明为什么inline函数要置于头文件,但Handle classes和Interface classes被设计用来隐藏实现细节。
我们要做的是,在程序中使用Handle classes和Interface classes,以求实现代码有所变化时,对其客户带来最小影响。但如果它们导致的额外成本过大,例如导致运行速度或对象大小差异过大,以至于classes之间的耦合相比之下不成为关键时,就以具体类(concrete classes)替换Handle classes和Interface classes。
总结
- 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义时。基于此构想的两个手段是Handle classes和Interface classes。
- 程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。不论是否这几templates,这种做法都是适用。
相关文章推荐
- 读书笔记《Effective C++》条款31:将文件间的编译依存关系降至最低
- 《Effective C++》学习笔记条款31:将文件间的编译依存关系降至最低
- 《Effective C++》之条款31:将文件间的编译依存关系降至最低
- Effective C++ 条款31 将文件中间的编译依存关系降至最低
- Effective C++ -----条款31:将文件间的编译依存关系降至最低
- Effectiv C++条款31 将文件间的编译依存关系降至最低 Handle Class和Interface Class完整实现
- Effective C++笔记_条款31将文件间的编译依存关系降至最低
- 条款31:将文件间的编译依存关系降至最低(Minimize compilation dependencies between files)
- effective C++ 条款 31:将文件间的编译依存关系降至最低
- 条款31:将文件间的编译依存关系降至最低
- Effective C++:条款31:将文件间的编译依存关系将至最低
- c90b 条款31:将文件间的编译依存关系降至最低
- 《Effective C++》读书笔记之item31:将文件间的编译依存关系降至最低
- Effectiv C++条款31 将文件间的编译依存关系降至最低 Handle Class和Interface Class完整实现
- Effectiv C++条款31 将文件间的编译依存关系降至最低 Handle Class和Interface Class完整实现
- 条款31:将文件间的编译依存关系降至最低
- 条款31:将文件间的编译依存关系降至最低
- Effective C++笔记_条款31将文件间的编译依存关系降至最低
- 条款31:将文件间的编译依存关系降至最低
- [Effective C++ --031]将文件间的编译依存关系降至最低