PIMPL-隐藏接口实现细节

前言

有时候我们需要提供对外的API,通常会以头文件的形式提供。举个简单的例子:
提供一个从某个指定数开始打印的接口,

头文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
//来源:公众号编程珠玑
//作者:守望先生
#ifndef _TEST_API_H
#define _TEST_API_H
//test_api.h
class TestApi{
public:
TestApi(int s):start(s){}
void TestPrint(int num);
private:
int start_ = 0;
};
#endif //_TEST_API_H

实现文件如下:

1
2
3
4
5
6
7
8
9
10
//来源:公众号编程珠玑
//作者:守望先生
#include "test_api.h"
#include <iostream>
//test_api.cc
TestApi::TestPrint(int num){
for(int i = start_; i < num; i++){
std::cout<< i <<std::endl;
}
}

类TestApi中有一个私有变量start_,头文件中是可以看到的。

1
2
3
4
5
6
#include "test_api.h"
int main(){
TestApi test_api{10};
test_api.TestPrint(15);
return 0;
}

常规实现缺点

从前面的内容来看, 一切都还正常,但是有什么问题呢?

  • 头文件暴露了私有成员
  • 实现与接口耦合
  • 编译耦合

第一点可以很明显的看出来,其中的私有变量start_能否在头文件中看到,如果实现越来越复杂,这里可能也会出现更多的私有变量。有人可能会问,私有变量外部也不能访问,暴露又何妨?

不过你只是提供几个接口,给别人看到这么多信息干啥呢?这样就会导致实现和接口耦合在了一起。

另外一方面,如果有另外一个库使用了这个库,而你的这个库实现变了,头文件就会变,而头文件一旦变动,就需要所有使用了这个库的程序都要重新编译!

这个代价是巨大的。

所以,我们应该尽可能地保证头文件不变动,或者说,尽可能隐藏实现,隐藏私有变量。

PIMPL

Pointer to implementation,由指针指向实现,而不过多暴露细节。废话不多说,上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//来源:公众号编程珠玑
//作者:守望先生
#ifndef _TEST_API_H
#define _TEST_API_H
#include <memory>
//test_api.h
class TestApi{
public:
TestApi(int s);
~TestApi();
void TestPrint(int num);
private:
class TestImpl;
std::unique_ptr<TestImpl> test_impl_;
};
#endif //_TEST_API_H

从这个头文件中,我们可以看到:

  • 实现都在TestImpl中,因为只有一个私有的TestImpl变量,可以预见到,实现变化时,这个头文件是基本不需要动的
  • testimpl是一个unique_ptr,因为我们使用的是现代C++,这里需要注意的一点是,它的构造函数和析构函数必须存在,否则会有编译问题。

我们再来看下具体的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//来源:公众号编程珠玑
//作者:守望先生
#include "test_api.h"
#include <iostream>
//test_api.cc
class TestApi::TestImpl{
public:
void TestPrint(int num);
TestImpl(int s):start_(s){}
TestImpl() = default;
~TestImpl() = default;
private:
int start_;
};

void TestApi::TestImpl::TestPrint(int num){
for(int i = start_; i < num; i++){
std::cout<< i <<std::endl;
}
}

TestApi::TestApi(int s){
test_impl_.reset(new TestImpl(s));
}

void TestApi::TestPrint(int num){
test_impl_->TestPrint(num);
}
//注意,析构函数需要
TestApi::~TestApi() = default;

从实现中看到,TestApi中的TestPrint调用了TestImpl中的TestPrint实现,而所有的具体实现细节和私有变量都在TestImpl中,即便实现变更了,其他库不需要重新编译,而仅仅是在生成可执行文件时重新链接。

总结

从例子中,我们可以看到PIMPL模式中有以下优点:

  • 降低耦合,头文件稳定,类具体实现改变不影响其他模块的编译,只影响可执行程序的链接
  • 接口和实现分离,提高稳定性

当然了,由于实现在另外一个类中,所以会多一次调用,会有性能的损耗,但是这点几乎可以忽略。

代码地址:源代码

守望 wechat
关注公众号[编程珠玑]获取更多原创技术文章
出入相友,守望相助!