首先,为什么要最小化编译器依赖呢?因为这样简化文件之间的依赖关系,减少由于一个文件的改变,而引起的另外的文件的重新编译。从而提高编译速度。下面来看一下下面的一个示例代码:
// x.h: original header
#include <iostream>
#include <ostream>
#include <list>
// None of A, B, C, D or E are templates.
// only A and C have virtual functions.
#include "a.h" // class A
#include "b.h" // class B
#include "c.h" // class C
#include "d.h" // class D
#include "e.h" // class E
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::osteam&) const;
private:
std::list<C> clist_;
D d_;
};
inline std::ostream& operator<<(std::ostream& os, const X& x)
{
return x.print(os);
}
下面分别列举一下以上代码中的问题:
1. 移除#include <iostream>
因为这里并不需要
2. 把#include <ostream>
替换成#include <iosfwd>
虽然我们的代码中用到了std::osteam
,但程序中都是作为参数的,所以并不需要包含ostream
, 只需要包含C++标准库提供给我们的类型声明头文件iosfwd
即可。
3. 移除#include "e.h"
,我们只需要前置声明一下就可以了,class E
。
4. 利用Pimpl-idiom
来进一步减少文件之间的依赖关系。
具体如下:
// x.h: after converting to use a Pimpl
// to hide implementation details
#include <iosfwd>
#include "a.h"
#include "b.h"
class C;
class E;
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::osteam&) const;
private:
struct XImpl;
XImpl* pimpl_;
};
inline std::ostream& operator<<(std:ostream& os, const X& x)
{
return x.print(os);
}
//implementation file x.cpp
struct X::XImpl
{
std::list<C> clist_;
D d_;
};
如上代码所示,我们把类X的private数据成员都已到了XImpl中,而在头文件中只保存了指向此结构体的一个指针,从而使我们可以不再包含#include <list>
、#include <C>
和 #include <D>
只需要前置声明就可以了。另外注意到类X是private继承自类B的,一般情况下private继承应该使用containment来实现,所以也应该把B移到XImpl中,这样只需要前置声明class B
就可以了。
从上面的代码可以看到,Impl-idiom
实际上是一种把实现和接口分离的一种方法,让调用X的外部类尽量少的知道类X的细节,外部调用只需要知道X的public函数就可以了,其他的都可以不必知道。所以可以把private的成员变量和函数放置到Pimpl
中。但同时也要注意,并不是所有private的成员变量和函数都可以放置到Pimpl
中的。一般private的成员变量可以移到里面去,但是virtual类型的private函数是不可以的。另外protected的成员和函数也是不可以的。
下面讨论一下上面这种方式的占用空间和效率问题。很明显,这种方式会让类X多一个指针变量,所以会增大类的占用空间,具体增大多少要考虑到具体的类和内存对齐问题。从效率上讲,主要有两方面的问题,一是类X在构造的过程中要动态的在堆上申请一块内存供Pimpl
使用,这相比于直接在类中声明成员变量,效率上会低一些。另外在访问具体的变量和函数时,也会多了一步指针的操作(dereference)。
当然,针对以上提出的效率问题,书中先是给出了一个错误的解决方案,如下:
class Y
{
/*...*/
static const size_t sizeofx = /*some value*/;
char x_[sizeofx];
}
//file y.cpp
#include "x.h"
Y::Y()
{
assert(sizeofx >= sizof(X));
new (&x_[0]) X;
}
Y::~Y()
{
(reinterpret_cast<X*>(&x_[0]))->~X();
}
为什么上面的实现方法是错误的呢?原因如下:
首先看下面一段代码:
char* buf1 = (char*)malloc(sizeof(Y));
char* buf2 = new char[sizeof(Y)];
char buf3[sizeof(Y)];
new (buf1) Y; // ok, buf1 allocated dynamically
new (buf2) Y; // ok, buf2 allocated dynamically
new (&buf3[0]) Y; // error, buf3 may not be suitably aligned
(reinterpret_cast<Y*>(buf1))->~Y(); // ok
(reinterpret_cast<Y*>(buf2))->~Y(); // ok
(reinterpret_cast<Y*>(&buf3[0]))->~Y(); // error
从以上代码可以看出,以上代码用类的成员变量预留空间的方式,虽然可以免去动态申请内存的开销,但会有内存对齐的问题,所以是错误的。
那么除了上面错误的方法,正确的做法是什么呢,书中给出的答案如下:
第一种方法;
struct X::XImpl
{
static void* operator new(size_t) {/*...*/}
static void operator delete(void*) {/*...*/}
};
X::x() : pimpl_(new XImpl){}
X::~X() { delete pimpl_; pimpl_ = 0; }
以上方法利用的是重载new和operator运算符,然后利用分配固定内存块的方式,对分配XImpl进行优化。
第二种方法:
这是比第一种方法更通用的方法.
//用于分配固定内存块的优化类
//比如可以先申请一块内存池,然后在此内存池上再分配内存
class FixedAllocator
{
public:
static FixedAllocator& Instance();
void * Allocate(size_t);
void Deallocate(void*);
}
struct FastArenaObject
{
static void* operator new(size_t s)
{
return FixedAllocator::Instance()->Allocate(s);
}
static void operator delete(void* p)
{
FixedAllocator::Instance()->Deallocate(p);
}
};
//我们只需要让XImpl继承自上面的类就可以了
struct X::XImpl : FastArenaObject
{
/*...private stuff here...*/
};
说了这么多,其实一般情况下,我们并不需要实现最后面这一种快速分配的方式。在效率不是那么紧急的情况下,我们只需要用通常的”Pimpl-idiom”就可以了,并不需要实现后面这种快速的方式。