首页 > 代码库 > 最大程度降低编译期依赖性(读书笔记)
最大程度降低编译期依赖性(读书笔记)
时间:2014.06.05
地点:基地
--------------------------------------------------------------------------------
一、识别多余的头文件
习惯性地使用#include指令包含一些不必要的头文件会严重降低编译效率,尤其是砸常用头文件中包含过多其它头文件时。比如:
//x.h 这是实现类X的头文件 // #include<iostream> #include<ostream> #include<list> //A,B,C,D,E都不是模板,A和C中定义了虚函数 #include "a.h" #include "b.h" #include “c.h" #include "d.h" #include "e.h" class X:public A,private B { public: x(const C&); B f(int,char*); C f(int,C); C& g(B); E h(E); virtual std::ostream& print(std::ostream&) const; private: std::list<C>clist_; D d_; }; inline std::ostream& operator<<(std::ostream& os,const X& X) { return x.print(os); }
1.1去掉#include<iostream>
这里在X类的声明中确实用到了流,但这里用到的流并不是iostream中的某个特殊声明,在X类中,最多只需要ostream就够了。也就是说永远不要用#include包含不必要的头文件。
1.2用iosfwd代替ostream
我们还可以进一步压缩头文件
对应函数的参数类型和返回值类型而言,我们只需要前置声明一下就可以了,于是在这里,真正需要的只是ostream的前置声明,而无需它的完整定义,因为这里确实仅仅涉及到ostream类当成函数参数类型和函数返回类型的一个声明,并不需要它的定义。但于这里,我们又不能简单的使用class ostream来代替#include<iostream>,因为:
a.ostream属于命名空间std,我们不能在std之外声明std中的类型。
b.ostream其实是一个模板的类型定义(由typedef得来的,即:typedef biasic_ostream<char> ostream)。
正因为诸如ostream其实是一个模板实例,模板的前置声明会给代码带来混乱,我们也不可能可靠地对模板进行前置声明,因为在库的实现中还可能包含C++标准之外的一些工作,比如额外的模板参数,这便是不允许对std名字空间中的类型进行声明的主要原因之一。
然而好在标准库中提供了头文件iosfwd,该文件包含了所有流模板(包括basic_ostream)以及这些模板类型定义(ostream)的前置声明,于是可用#include<iosfwd>代替#include<ostream>
如果只需要流的前置声明,优先使用#include<iosfwd>
我们再来看那个内联函数:
inline std::ostream& operator<<(std::ostream& os,const X& x) { return x.print(os); }这个内联函数中,参数类型和返回类型都是ostream& ,正如上面所说,显然不要ostream的定义,然后ostream& 参数将作为另一个函数的参数供它调用,这一步也是无需知道ostream的定义的。但当调用ostream的成员函数时就需要完整的定义了。
1.3用前置声明来代替e.h
前面代码我们看到,代码中使用了#include "e.h"来引用类E,但事实上,类E只是被用来声明参数类型和返回类型,因此不需要E的完整定义。并且在x.h文件中也不应该首先引入e.h头文件,我们只需用class E来代替#include.h
只需要前置声明时,绝对不要用#include包含相应的头文件。
通过上面3步修改,我们得到下面代码
//x.h:删除了不必要的头文件 // #include<iosfwd> #include<list> //ABCD都不是模板,只有AC定义了虚函数 #include "a.h" #include "b.h" #include "c.h" #include "d.h" class E; //现在用前置声明代替了原来的 #include "e.h" class X:public A,private B { public: X(const C&); B f(int,char*); C f(int,C); C& g(B); E h(E); virtual std::ostream& prrint(std::ostream& )const; private: std::list<C> clist_; D d_; }; inline std::ostream& operator<<(std::ostream& os,const X& x) { return x.print(os); }
1.4 Pimpl(句柄/本体惯用法)
在上面代码中,我们看到保留a.h和b.h是必须的,不能删除这两个文件,这是因为X继承于类A和类B,因此在头文件中必须有基类的完整定义,这样编译器才能确定X对象的大小,虚函数以及其它基本信息。
二个方面,我们也必须保留list c.h 和d.h文件,这是因为list<C>和D被用作X的私有数据成员,虽然C既不是基类也不是数据成员,但它被用来实例化数据成员list,主流编译器中都要求实例化list<C>时编译器能看到C的定义。
在这里,我们可容易地将类的私有部分封装起来,防止未授权的访问,但不幸的是在这里并没有将私有部分的依赖性封装。封装的要点在于:客户代码无需去了解或关心私有部分的实现细节。然而我们在类的头文件中还是可以看到类的私有部分,于是客户代码不得不依赖在这些私有部分中会使用到的所有类型。
我们的解决办法是设置一个Pimpl指针,指向一个进行了前置声明但没有定义的辅助类来隐藏类的私有成员,之前我们都是这样声明X类的:
//x.h class X { public: //公有成员和保护成员 private: //私有成员,当私有成员发生变化时 //所有客户代码都必须重新编译 };利用Pimpl惯用法,我们写成这样
//x.h class X { public: //公有数据成员 private: struct XImpl; //前置声明类 XImpl* pimpl_;//指向前置声明类的指针 }; //x.cpp struct X::XImp; { //私有成员完全被隐藏起来,当私有成员发生改变时 //客户代码无需重新编译 };在每个X对象中包含的XImpl对象都是动态分配的,如果将对象看做一个物理内存块,使用Pimpl的做法相当于从根本上削减对象中的大部分内存块,用另一个小得可怜的小内存块来代替,即这个不透明的指针Pimpl。
Pimpl惯用法的优点在于打破了编译期的依赖性。
在私有部分使用的类型定义,只有在类的实现中才会需要,而在客户代码中时不需要的,这样可削减额外的#include指令并提高编译速度
类的实现可以被修改,可自由增加或删减类的私有成员,而客户代码无需重新编译。
Pimpl惯用法的主要开销在于程序的执行性能
在每个构造/析构过程中都必须分配/释放内存
在每次访问被隐藏起来的成员时,至少需要一次额外的间接操作,存在多重间接操作。
对于被广泛使用的类,应该优先使用编译器防火墙惯用法(Pimpl惯用法)来隐藏实现细节,通过一个不透明的指针指向一个进行了前置声明但没有定义的类)来保存私有成员(包括状态变量和成员函数),在声明使用Pimpl惯用法时刻如下:
...... struct MyClassImpl;//前置声明 MyClass* pimpl_; ......
于是乎,现在代码简化如下:
#include<iosfwd> #include "a.h" #include "b.h" class C; class E; class X:public A,private B { ...... };
//file x.cpp中的实现 struct X::XImpl { std::list<C> clist_; D d_; };
1.5根据需要使用强的关系而不是更强
继续分析代码我们发现,X在从A派生时使用公用继承,从B派生时使用私有继承。公用继承是对IS-A关系的建模,并满足Liskov替换原则。但这里B是私有继承,如果选择私有继承而不是聚合,那是为了获得对包含成员的访问。继承大多数情况下意味着要覆盖虚函数。然而这里B中没有声明虚函数,于是在这里也没必要选择继承这种更强的关系,如果X需要访问B中的包含成员函数或保护数据成员,那就得使用继承,在这里对类的继承是不必要的。于是有这样一条原则:如果使用聚合关系就已经足够,就不要使用继承
现在部分代码如下:
#include<iosfwd> #include "a.h" //类A的定义,因为存在继承虚函数覆盖所以必不可少 class B; class C; class E; class X:public:A { ...... };