谈谈pimpl模式
相信大家在阅读C++代码的时候,一定都见过pImpl的实现方式。我第一次见到pImpl实现方式时,对于下面这种代码:
void MyClass::SomeFunc() { pImpl_->SomeFunc(); }
总有一种“脱了裤子放屁”的感觉。而对于经常提到的pImpl模式的优点,什么“屏蔽私有接口”,什么“增加封装性”,又觉得虚之又虚——我这系统要么给自己部门用的,要么是要开源的,有啥见不得人的,要屏蔽呢?多增加了一层pImpl也没见得封装性有什么提升啊?
其实,pImpl模式既然应用如此广泛,必然有其价值。查阅资料、深入思考之后,我觉得它的好处主要是如下两点。
编译速度友好
其实上面说的“屏蔽私有接口”,更重要的是“编译防火墙”的作用。
C++程序员一定都受过编译速度的折磨。一个build敲下去,就可以起身去瑞幸咖啡薅一把羊毛了。那有经验的程序员都知道,前置声明可以大大加快编译速度:对于指针/引用,编译期只需要知道它占几个字节,把内存对象布局搞定就可以了,并不需要知道实现细节。
那么如果把一个类的实现细节,用一个Impl类封装起来,并且使用前置声明,放到对外的接口类中。那么对外接口类应该是这样的:
// MyClass.h Class MyClass { public: // Interface functions, such as void SomeFunc(); private: Class Impl; std::unique_ptr pImpl_; }
如此这般,当我修改Impl类中的函数实现细节时,所有include “MyClass.h”的文件,均不会需要重新编译,因为整个MyClass对象的内存布局是和Impl的实现无关的。
所以我认为,pImpl最突出的一个优势,是可以大大加快编译速度。而正好,这种实现方式,隐藏了实现的细节。对于一些闭源软件库来说,正好,也可以避免暴露实现细节。
移动语义友好
C++11中引入了移动语义,可以参见我之前的一篇文章 《再谈右值引用与移动语义》 。那pImpl模式,对移动语义也是友好的。
对于不使用pImpl模式的类来说,要实现移动语义,需要对每一个数据成员变量进行处理;更糟糕的是,每次为接口类MyClass新增一个数据成员(这肯定是很常见的事情),都需要记得,要去修改移动语义相关的函数。相信我,你一定会忘记的。
用了pImpl就不一样了,对于移动语义,只需要“移动”一个指针即可,高效且一劳永逸~后面再增加数据成员,也是Impl类内部的事情,接口类的移动语义无需变动!完美~
损失
凡事都有两面性,pImpl模式肯定也有缺点。我这里想到两点:
- 调用的间接性
- 接口类函数的实现,一定要经过pImpl指针转一道,性能肯定有损耗,增加新接口写起来也麻烦;增加了一个指针的大小,虽然看上去不是什么大问题,但是对于每个对象都比较小,但是对象数量很多的那种类,这样的空间开销也不可忽视。
- const问题
- 另外,const是一个比较严重的问题。类的const函数可以确保不修改该类的数据成员变量,但是pImpl模式下,本来属于接口类的数据成员,全部被Impl类承包了。这时,接口类的const函数,只能确保pImpl 指针不被修改,而对于pImpl 指针所指向对象的成员变量,则无法强制保持const。
- 这个问题可以通过
std::experimental::propagate_const
包装来修复++// MyClass.h Class MyClass { public: // Interface functions, such as void SomeFunc(); private: Class Impl; std::experimental::propagate_const<std::unique_ptr> pImpl_; }
总结
pImpl是C++程序员经常遇到的一种编程模式,主要用于建立“编译防火墙”。同时,带来了屏蔽私有接口、移动语义友好等优点;
对于const函数被非const指针所屏蔽的问题,可以通过 std::experimental::propagate_const
来解决。
转载请注明出处: http://blog.guoyb.com/2019/06/07/pimpl/
欢迎使用微信扫描下方二维码,关注我的微信公众号TechTalking,技术·生活·思考: