Guru of the Week #7:编译时期的依赖关系(Compile-time Dependences)
作者:Herb Sutter
译者:plpliuly
/*此文是译者出于自娱翻译的GotW(Guru of the Week)系列文章第7篇,原文的版权是属于Herb Sutter(著名的C++专家,"Exceptional C++"的作者)。此文的翻译没有征得原作者的同意,只供学习讨论。――译者
*/
#7 编译时期的依赖关系(Compile-Time Dependencies)
难度:7/10
很多人在写C++程序时都会用#include包含一些没有必要的头文件。你是否也是这样呢?看看我们下面要讨论的问题就知道了。
问题:
[提醒:这个问题会比看起来要难。另外请注意程序中的注释。]
包含过多不必要的头文件会严重增长程序build时间,尤其是在一个被很多源文件包含的头文件中#include了太多的头文件。
在下面的头文件中,哪些#include语句可以立即去除掉?第二,哪些#include语句可以在作适当调整后去除?(你不能改变class X和class Y的公共接口;也就是说,你做的任何更改不能影响客户代码)。
// gotw007.h (implementation file is gotw007.cpp)
//
#include "a.h" // class A
#include "b.h" // class B
#include "c.h" // class C
#include "d.h" // class D
// (note: only A and C have virtual functions)
#include <iostream>
#include <ostream>
#include <sstream>
#include <list>
#include <string>
class X : public A {
public:
X ( const C& );
D Function1( int, char* );
D Function1( int, C );
B& Function2( B );
void Function3( std::wostringstream& );
std::ostream& print( std::ostream& ) const;
private:
std::string name_;
std::list<C> clist_;
D d_;
};
std::ostream& operator<<( std::ostream& os, const X& x )
{ return x.print(os); }
class Y : private B {
public:
C Function4( A );
private:
std::list<std::wostringstream*> alist_;
};
答案
首先,看看哪些头文件完全没必要包括进来.
1。我们可以立即去掉:
-iostream,因为尽管用了streams,但并没有用到任何iosteam中任何特殊的东西。
-osteam和sstream,因为参数和返回值都只需用前递类型声明(forward-declared)就可以了。因此只需包括iosfwd就可以了.(注意没有对应的"stringfwd"或"listfwd"标准库头文件;iosfwd是为了向下兼容已有的使用非模板stream子系统的代码)
我们不能立即去除的头文件有:
-a.h:因为A是X的基类
-b.h:因为B是Y的基类
-c.h:因为很多现有的编译器要求在实例化list<C>时知道C的定义(在以后的编译器版本中应该修正这点)
-d.h:list和string,因为X类需要知道D和string的对象大小,而且X和Y都需要知道list的对象大小。
然后,让我们看看哪些头文件可以在做适当调整后去掉:
2。我们可以通过让X和Y使用pimpl_来去处d.h,list和string(也就是说,私有部分用指向实现对象的指针代替,实现对象是前递声明(forward-declared)的),因为这样X和Y就不需要知道D和list,string的大小信息了。这也使得我们可以不需包括c.h,因为在X::clist_中C的对象只作为参数或者返回值出现。
重要的提示:定义为内联类型的操作符<<可以保持为内联的并使用ostream类型的参数,尽管ostream还没有定义!这是因为你只在要调用成员函数的时候才需要参数的定义,而当你只是需要用一个对象来作为函数的参数时并不需要知道该对象的定义。
最后,看看是否可以继续调整以去除更多的头文件:
3。注意到B是Y的私有基类而且B没有虚函数,我们又可以去除b.h。通常选择私有继承而不选择类聚合或包容(嵌套类)的主要原因就是为了重新定义基类的虚函数。因此,此处应该改为让Y有一个B类型的成员变量来代替从B类私有继承。为了去除b.h,这个成员变量应该是在Y的被隐藏的pimpl_部分。
[建议]尽量用pimpl_(也就是指向实现的指针)将实现细节和客户代码隔离开来。
下面的文字是从GotW 的代码规范中摘录出来的:
-封装和隔离
-避免在类的声明中直接定义私有成员
-使用一个非透明指针声明为“struct XxxxImpl* pimpl_”来存储私有成员(包括私有成员变量和成员函数),比如,class Map{
private:struct MapImpl* pimpl_;}(Lakos96:398-405;Meyer92:111-116;Murray93:72-74)
4.目前我们还不能对a.h做任何动作,因为A被用来做X的public基类,而且A有虚成员函数,因此客户代码中需要知道或使用这种IS-A关系。然而,注意到X和Y根本不相关,我们可以将X和Y的定义分离到两个不同的头文件中(假设把现在这个头文件改成一个包括x.h和y.h的存根,因此就不需要改变现有的代码)。这样的话,至少y.h不需要包括a.h,因为它只是使用了A作为函数参数,那是不需要定义的。
把上述内容综合起来,我们可以得到如下几个更加信息的头文件:
file://---------------------------------------------------------------
// new file x.h: only TWO includes!
//
#include "a.h" // class A
#include <iosfwd>
class C;
class D;
class B;//译者加
class X : public A {
public:
X ( const C& );
D Function1( int, char* );
D Function1( int, C );
B& Function2( B );
void Function3( std::wostringstream& );
std::ostream& print( std::ostream& ) const;
private:
class XImpl* pimpl_;
};
inline std::ostream& operator<<( std::ostream& os, const X& x )
{ return x.print(os); }
// NOTE: this does NOT require ostream's definition!
file://---------------------------------------------------------------
// new file y.h: ZERO includes!
//
class A;
class C;
class Y {
public:
C Function4( A );
private:
class YImpl* pimpl_;
};
file://---------------------------------------------------------------
// gotw007.h is now just a compatibility stub with two lines, and
// pulls in only TWO extra secondary includes (through x.h)
//
#include "x.h"
#include "y.h"
file://---------------------------------------------------------------
// new structures in gotw007.cpp... note that the impl objects
// will be new'd by the X/Y ctors and delete'd by the X/Y dtors
// and X/Y member functions will access the data through their
// pimpl_ pointers
//
struct XImpl // yes, this can be called "struct" even
{ // though the forward-decl says "class"
std::string name_;
std::list<C> clist_;
D d_;
}
struct YImpl
{
std::list<std::wostringstream*> alist_;
B b_;
}
从上面的几个头文件中可以看出,使用X的客户代码(源文件)只需包含a.h和iostwd;在不改变客户代码情况下,使用Y的客户代码也只需要包含a.h和iosfwd,如果日后使用Y的客户代码更新将包含gotw007.h改为包含y.h,那Y的客户代码就不需要为使用Y而包含任何别的头文件。比较一下原来的头文件,这是一个多大的改进!