Pimpl idiom惯用法C++11

2020年02月03日 241点热度 0人点赞 0条评论

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

pimpl 惯用法

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

// x.h

class X{
public:
   void func();
private:
   class XImpl;
   Ximpl *impl_;
};

// x.cpp
class X::XImpl{
  // ::: 
  // implement details here
};

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

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

  • 编译防火墙。 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 file
class widget {
public:
    widget();
    ~widget();
private:
    class impl;
    unique_ptr<impl> pimpl;
};

// in implementation file
class 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的析构函数。通过把析构函数写在实现文件中,强制定义在 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

Charles

Stay hungry, stay foolish.

文章评论