写点什么

C++ 中的 pimpl 惯用法

作者:行者孙
  • 2021 年 12 月 14 日
  • 本文字数:2030 字

    阅读完需:约 7 分钟

这篇给大家介绍什么是 PImpl 惯用法,以及使用 std::unique_ptr 实现,并且实现了该的复制和赋值构造函数

pimpl 惯用法

在很多 C++ 的 API 源码里,我们经常看到接口类通常指包含公有方法,而真正的实现类通常用一个不透明指针 (opaque pointer) 指向。例如


// x.h
class X{public: void func();private: class XImpl; Ximpl *impl_;};
// x.cppclass X::XImpl{ // ::: // implement details here};
复制代码


这种方式称为 pimpl 惯用法(pimpl idiom)。有时也称为编译防火墙(compliation firewalls)。

好处(为什么使用这种技巧)

  • 编译防火墙。C/C++ 里有个特点——只要类的定义变了(即使是私有成员),所有 include 该类定义的头文件的 cpp 文件都要被重新编译,这将导致大型 C++项目编译时间过长。Pimpl 隐藏了私有成员,在修改 XImpl 实现时不需要重新编译客户代码;

  • 隐藏实现细节,接口与实现分离。因为 .h 往往是暴露给用户的,好的 API 设计往往向用户隐藏实现细节。这样实现的修改、优化与接口分离,更加稳定健壮。

如何实现

把暴露给用户的类称为接口类 X,实现类称为 XImpl, 有以下几个要点,


  1. 实现类 XImpl, 往往声明为私有内嵌类,即 XImpl 声明在 class X 的内部并且是 private 的;

  2. 实现类的 XImpl 实例的访问通过指针进行;

  3. 注意内存泄露,即XImpl *impl中的 impl 在被析构时应当 delete,建议通过 RAII 的方式,在 C++11 及以后可以使用智能指针 std::unqiue_ptr

  4. 如果 X 的语义是可以复制的或可移动的,实现者需要自己实现复制、赋值或移动构造函数。如果是不可复制或移动的,相应的构造函数应该声明为 delete(禁止 copy/move 构造等)。

  5. 在 XImpl 一般放原先 X 中的私有成员和方法,对于 virtual 函数必须放在 X 中,以保证公有类能够 override。


举个例子:


// in header fileclass widget {public:    widget();    ~widget();private:    class impl;    unique_ptr<impl> pimpl;}; // in implementation fileclass widget::impl {    // }; widget::widget() : pimpl{ new impl{ /*...*/ } } { }widget::~widget() { }                   // or =default
复制代码


需要自己定义 widget 的析构函数并且放到 impl 的定义之后,哪怕和 default 一样: 原因是


unique_ptr’s destructor requires a complete type in order to invoke delete (unlike shared_ptr which captures more information when it’s constructed). By writing it yourself in the implementation file, you force it to be defined in a place where impl is already defined, and this successfully prevents the compiler from trying to automatically generate the destructor on demand in the caller’s code where impl is not defined.


翻译过来就是: unique_ptr<T> 中需要一个完整的模板类型 T,从而可以调用 T 的析构函数。通过把析构函数写在实现文件中,强制定义在 impl 之后,从而使得此时的 T 也即 impl 是完整的类型。否则编译器将会报错。

实例

已在 vs2017 下编译通过。源码见我的github


// x.h#include <memory> // std::unique_ptr#include <string>class X{public:    X();    ~X();    X(const X& x);    X& operator=(const X& x);    void push_element(const std::string& str);    void print();private:    class XImpl;    std::unique_ptr<XImpl> pimpl_;};
复制代码


// x.cpp
#include "x.h"#include <vector>#include <iostream>#include <string>class X::XImpl{public: XImpl() = default; std::vector<std::string> array_;};
X::X() :pimpl_{new XImpl}{
}
// or =default; This is neccessary,because// unique_ptr’s destructor requires a complete type in order to invoke delete;// here force it to be defined in a place where impl is already defined// see: https://herbsutter.com/gotw/_100/X::~X(){}
X::X(const X &x){ pimpl_ = std::make_unique<XImpl>(); *pimpl_ = *x.pimpl_;}
X &X::operator=(const X &other){ if(&other == this){ return *this; } *pimpl_ = *other.pimpl_; return *this;}
void X::push_element(const std::string &str){ pimpl_->array_.push_back(str);}
void X::print(){ for(const auto & i :pimpl_->array_){ std::cout<< i <<" "; } std::cout<<std::endl;}
复制代码


实例类 X,可以向里面 push 字符串,然后 print()方法会打印出所有字符串。主要演示了 Pimpl 惯用法的实现,特别要注意需要在 x.cpp 里定义 X 析构函数,并且要放在 XImpl 类的定义之后,否则编译器会报错“allocation of incomplete type, type XImpl is incomplete”。


另外因为 unique_ptr是不可拷贝的,所以默认情况下 X 也是不可拷贝的,为了支持 copy 语义,手动实现了 copy 构造函数和 copy-assignment operator。

Ref

  1. https://herbsutter.com/gotw/_100/

  2. http://www.gotw.ca/gotw/024.htm

发布于: 37 分钟前阅读数: 6
用户头像

行者孙

关注

Nothing replaces hard work 2018.09.17 加入

充满好奇心,终身学习者。 博客:https://01io.tech

评论

发布
暂无评论
C++中的pimpl惯用法