Pointer to Implementation (PImpl) 是一种设计技巧(idiom),使用这种模式,在开发库的过程中,我们可以去掉头文件中的私有变量和执行依赖,仅保留接口。
cppreference对于PImpl的解释:https://en.cppreference.com/w/cpp/language/pimpl
PImpl至少有以下几个好处:
我们先说实现,然后再解释上面的好处。
首先,用户只能看到A的定义部分,A::Impl的定义的实现都是看不到的,到达了我们的第一个目的。
其次,因为A的定义只包含接口部分,只要接口不动,不管实现怎么改动,用户代码都不会被重新编译,在一些库和用户代码同时开发的情况下,能加快编译速度。
最后,我们来看一下第3点好处,首先,什么是依赖传递到用户代码? 我们在代码中加入了第三方头文件以及它提供的类型third_party_type_t,我们需要使用这种类型。 (这里我们用注释注掉了,因为这样代码才能编译运行) 如果我们不用PImpl方式,那么third_party_type_t类型必需定义在接口头文件中,对应的“3rd_party_header.h”也必需定义在接口头文件中。 这样,只要我们只要包含接口文件,就必须在编译期间提供第三方库的路径。
有人会说:包含就包含呗,有什么大不了的,反正都得用。
为了解释,我们这举个例子,假设我们的库包含好几个模块,统一的头文件内容为:
此时此刻,用户代码只需要“module_b”的功能,并且提供了y的编译配置,但是此时一旦包含整个“my_lib.h”,就会因为缺少第三方库x和z的路径而导致编译报错。
显然,此时让用户代码去提供x和z的编译配置肯定是不科学的,都没用到这些功能,为啥要管他,而且,万一模块很多呢,岂不是浪费时间?
另一种方法就是用户自己去甄别需要的模块,然后去包含,在这里就是“::include "module_b.h"”, 但是,我们在编写库的时候,假设库有很多模块,我们更希望的是提供一个统一的头文件,而不是提供很多个单独的头文件,让用户花时间去学习和选择。
用了PImpl,我们就可以在提供统一一个头文件的情况下,让用户只用自己关心的功能,不要关心其他模块。
【事项1】使用std::unique_ptr来定义指针,不用裸指针的原因就不用说了,2020年了,再用裸指针就说不通了。 为什么要用unique_ptr,这是因为我们希望接口类和实现的类是意义绑定的,让别的实例指向实现没有任何意义。
【事项2】使用std::unique_ptr时,接口类的析构函数必需显式定义,即下面这句话是必需的。
如果不这样做,单独编译库是没问题的,但是库在使用的时候,就会报错。这是因为需要给编译器一个定义unique_ptr析构函数的地方。