Pimpl 模式

​ Pimpl (pointer to implementation) 是一种将接口和具体的实现分开的一种方式,优化编译时间,保护封装实现的一种经验技巧。

tl;dr

​ 就是说模块对外接口类里面不放实现,而是保存一个指针, 指向真正实现的类。然后在接口类里面同名方法通过指针调用真正实现方法。

原理

​ 通过指针的间接调用,实现消息的转发,使得行为与原来行为相同。

Demo

普通类的设计

​ 假设原来有这样一个类:

1
2
3
4
5
6
7
8
9
// File: Foo.h
class Foo {
public:
Foo (int data);
virtual ~Foo ();

private:
int data_;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
// File: Foo.cpp
#include "Foo.h"

Foo::Foo(int data) : data_(data) { }
Foo::~Foo() { }

void Foo::SetData(int data) {
data_ = data;
}

int Foo::Data() {
return data_;
}

Pimpl模式

在Foo的基础上在抽象出一层接口:

1
2
3
4
5
6
7
8
9
10
11
12
// File: Foo.h
class Foo {
public:
Foo (int data);
virtual ~Foo ();
virtual void SetData(int data);
virtual int Data();

private:
class FooImpl;
std::unique_ptr<FooImpl> foo_;
};
"Foo.h"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// File: Foo.cpp
#include "Foo_impl.h"

Foo::Foo(int data) {
foo_ = std::make_unique<FooImpl> (data);
}

Foo::~Foo() { }

void Foo::SetData(int data) {
foo_->SetData(data);
}

int Foo::Data() {
return foo_->Data();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// File: Foo_impl.h
#include "Foo.h"

class Foo::FooImpl {
public:
FooImpl (int data);
virtual ~FooImpl ();
virtual void SetData(int data);
virtual int Data();

private:
int data_;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// File: Foo_impl.cpp
#include "Foo.h"
#include "Foo_impl.h"

Foo::FooImpl::FooImpl(int data) : data_(data) { }
Foo::FooImpl::~FooImpl() { }

void Foo::FooImpl::SetData(int data) {
data_ = data;
}

int Foo::FooImpl::Data() {
return data_;
}

优缺点:

优点:

  • 优化编译速度, 如上面的例子, 普通的实现方式,在设计类时不可避免要使用一些其他的类或者变量,就会引入很多头文件。那么在编译时,一个头文件的引入可能就会增加一杯咖啡的时间(例如windows.h), 而Pimpl不会, 因为其他模块在编译时只需要引入Foo.h头文件即可, 里面之后告诉编译器有个FooImpl的类, 没有很复杂的东西, 这时就不会引入很多的东西,编译时间就会短很多。
  • 优化构建过程,无论我们怎么修改FooImpl, 只要Foo.h不变,那么就不会影响到使用的模块,编译时只需要编译FooImpl部分即可。
  • 对具体实现的隐藏,模块输出对外接口时,头文件只会看到一个FooImpl的指针, 看不到类里面的内容。
  • 对单元测试友好, 测试时只需要测试Impl部分即可, 因为Foo只是个接口。 如果测试另一个模块,Foo也可以加一个Only for Test的构造,来模拟真实的FooImpl, 避免过度耦合无法测试, 例如socket, 很多人喜欢在负责传输的模块内部直接写一个socket, 这样在测试时很难去模拟网络IO, 用Impl就很好的解决了这个问题。

缺点:

  • 写起来更麻烦, 写的更多了, 但这好像不是什么太大的问题
  • 增加开销, 因为多了一层调用
  • 增加了内存的开销, 多了一个智能指针

   


参考:

​ Effective C++ & More Effective C++