栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

Item 22: When using the Pimpl Idiom, define special member functions in the implementation file.

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Item 22: When using the Pimpl Idiom, define special member functions in the implementation file.

Item 22: When using the Pimpl Idiom, define special member functions in the implementation file.

PImpl 技术(编译防火墙)PImpl 技术的智能指针版本

Effective Modern C++ Item 22 的学习和解读。

这部分介绍一个智能指针的应用场景:PImpl技术,俗称编译防火墙。

PImpl 技术(编译防火墙)

PImpl(Pointer to implementation)是一种 C++ 惯用技术,它是通过将类的具体实现放在另一个单独的类(或结构体)中,并通过不透明的指针进行访问。该技术能够将具体的实现细节从对象中去除,能够有效地减少编译依赖。也被称为“编译防火墙(Compilation Firewalls)”。看一个例子:

class Widget {  // in header "widget.h"
public:
  Widget();
  ...
private:
  std::string name;
  std::vector data;
  Gadget g1, g2, g3;        // Gadget is some user-defined type
};

这里, Widget 中包含 std::string、std::vector 和 Gadget 类型成员变量,对于 Widget 的客户,必须包含 和 gadget.h 这些头文件。一旦这些头文件的内容发生改变(当然,string 和 vector 两个头文件的内容很少被修改),使用 Widget 的客户代码必须重新编译。

PImpl 技术可以很好的解决这个问题。将 Widget 类中的数据成员变量封装成一个类(或结构体),然后将数据成员替换成这个类(或结构体)的指针。

class Widget { // still in header "widget.h"
public:
  Widget();
  ~Widget();  // dtor is needed—see below
  ...
private:
  struct Impl;  // declare implementation struct
  Impl *pImpl;  // and pointer to it
};

Widget 类中不在提及 std::string、std::vector 和 Gadget 类型,因而无需包含相应的头文件。即使这些头文件的内容发生改变, Widget 客户代码也无需重新编译。

一个只声明不定义的类型被成为不完整类型(incomplete type),Widget::Impl 就是这样的一个类型,声明一个指向它的指针是可以编译的,PImpl 技术就是利用了这一点。PImpl 技术的典型方式如下:

前向声明一个类型,然后申明一个指向这个类型的指针。在原始类的实现文件中定义这个类型,并实现这个指针的动态内存分配和回收。

#include "widget.h"   // in impl. file "widget.cpp"
#include "gadget.h"
#include 
#include  

struct Widget::Impl {
  std::string name;            // definition of Widget::Impl
  std::vector data;    // with data members formerly in Widget
  Gadget g1, g2, g3; 
}; 

Widget::Widget()    // allocate data members for
: pImpl(new Impl)   // this Widget object
{} 

Widget::~Widget()   // destroy data members for
{ delete pImpl; }   // this object

这样,就把 和 gadget.h 这些头文件的依赖从 widget.h (Widget 的客户可见)中转移到了 widget.cpp (Widget 的实现者可见)中。

以上就是 Pimpl 技术的基本原理,这里都是直接使用原始指针,完全是 C++98 风格的实现,C++11 之后,我们更倾向使用智能指针来代替原始指针。

PImpl 技术的智能指针版本

这里,std::unique_ptr 是比较合适用来替换原始指针的,我们修改上面的代码:

// in "widget.h"
#include 
class Widget {
public:
  Widget();
  ...
private:
  struct Impl;                 
  std::unique_ptr pImpl;  // use smart pointer instead of raw pointer
};

//==================================================================================//
// in "widget.cpp"
#include "widget.h"  
#include "gadget.h"
#include 
#include 

struct Widget::Impl {  // as before
  std::string name;
  std::vector data;
  Gadget g1, g2, g3;
};

Widget::Widget()
: pImpl(std::make_unique())
{}

由于智能指针的自动析构指向的资源,这里无需析构函数。

#include "widget.h"
int main() {
  Widget w;
  return 0;
}  

但是,以上代码会编译报错,报错信息如下:

In file included from /usr/include/c++/9/memory:80,
                 from widget.h:3,
                 from hello.cpp:1:
/usr/include/c++/9/bits/unique_ptr.h: In instantiation of ‘void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Widget::Impl]’:
/usr/include/c++/9/bits/unique_ptr.h:292:17:   required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Widget::Impl; _Dp = std::default_delete]’
widget.h:5:7:   required from here
/usr/include/c++/9/bits/unique_ptr.h:79:16: error: invalid application of ‘sizeof’ to incomplete type ‘Widget::Impl’
   79 |  static_assert(sizeof(_Tp)>0,
      |   

编译报错分析如下:

在 main 函数结束时候,离开了 w 的作用域,w 将被销毁,也即会调用 Widget 的析构函数。我们的代码虽然没有定义 Widget 的析构函数,但是根据 Item 17 的介绍可以知道,编译器会生成默认的析构函数(inline 的)。析构函数中会释放 pImpl,pImpl 的是 std::unique_ptr 类型。而 std::unique_ptr 默认使用 delete 析构,默认的 delete 代码中会使用 static_assert 确保指针不会指向一个不完整类型,这里用到了 sizeof,而一个不完整类型无法进行 sizeof。

  /// Primary template of default_delete, used by unique_ptr
  template
  struct default_delete
  {   
      /// Default constructor
      constexpr default_delete() noexcept = default;

      
      template::value>::type>
        default_delete(const default_delete<_Up>&) noexcept { } 

      /// Calls @c delete @p __ptr
      void operator()(_Tp* __ptr) const
      {   
		  static_assert(!is_void<_Tp>::value,
		          "can't delete pointer to incomplete type");
		  static_assert(sizeof(_Tp)>0,
		          "can't delete pointer to incomplete type");
		  delete __ptr;
      }   
  };  

所以,要解决这个问题,只需要保证在销毁 std::unique_ptr 时,Impl 是一个完整类型即可,也即有定义。Impl 的定义在 Wigdet.cpp 文件中,因此我们只需让编译器在 Wigdet.cpp 生成析构函数即可。

// in "widget.h"
#include 
class Widget {
public:
  Widget();
  ~Widget();   // declaration only
  ...
private:
  struct Impl;                 
  std::unique_ptr pImpl;  // use smart pointer instead of raw pointer
};

//==================================================================================//
// in "widget.cpp"
#include "widget.h"  
#include "gadget.h"
#include 
#include 

struct Widget::Impl {  // as before
  std::string name;
  std::vector data;
  Gadget g1, g2, g3;
};

Widget::Widget()
: pImpl(std::make_unique())
{}

Widget::~Widget() {}  // ~Widget definition
// Widget::~Widget() = default;  // same effect as above

由于智能指针的自动析构指向的资源,这里无需析构函数。

#include "widget.h"
int main() {
  Widget w;
  return 0;
}  

这样就可以编译通过了。但是上面的实现还有点问题:从 Item 17 介绍我们知道,析构函数的声明会阻止编译器生成 move 操作,那么以上代码将不支持 move 操作了。解决办法也是相似的:

// in "widget.h"
#include 
class Widget {
public:
  Widget();
  ~Widget();   // declaration only
  
  Widget(Widget&& rhs);              // declarations
  Widget& operator=(Widget&& rhs);   // only
  ...
private:
  struct Impl;                 
  std::unique_ptr pImpl;  // use smart pointer instead of raw pointer
};

//==================================================================================//
// in "widget.cpp"
#include "widget.h"  
#include "gadget.h"
#include 
#include 

struct Widget::Impl {  // as before
  std::string name;
  std::vector data;
  Gadget g1, g2, g3;
};

Widget::Widget()
: pImpl(std::make_unique())
{}

Widget::~Widget() {}  // ~Widget definition
// Widget::~Widget() = default;  // same effect as above

Widget::Widget(Widget&& rhs) = default;
Widget& Widget::operator=(Widget&& rhs) = default;

这样 Widget 具备了 move 操作。但是还是存在一点问题:

根据 Item 17 知道,因为自定义了 move 操作,将会阻止编译器生成 copy 操作。即使编译器生成了 copy 操作(使用 = default 进行声明),也是一个浅拷贝,std::uniqe_ptr 是一个所有权独享的对象,对它进行拷贝会转移所有权。

因此,需要我们自定义 copy 操作:

// in "widget.h"
#include 
class Widget {
public:
  Widget();
  ~Widget();   // declaration only
  
  Widget(Widget&& rhs);              // declarations
  Widget& operator=(Widget&& rhs);   // only

  Widget(const Widget& rhs);               // declarations
  Widget& operator=(const Widget& rhs);    // only
  ...
private:
  struct Impl;                 
  std::unique_ptr pImpl;  // use smart pointer instead of raw pointer
};

//==================================================================================//
// in "widget.cpp"
#include "widget.h"  
#include "gadget.h"
#include 
#include 

struct Widget::Impl {  // as before
  std::string name;
  std::vector data;
  Gadget g1, g2, g3;
};

Widget::Widget()
: pImpl(std::make_unique())
{}

Widget::~Widget() {}  // ~Widget definition
// Widget::~Widget() = default;  // same effect as above

Widget::Widget(Widget&& rhs) = default;
Widget& Widget::operator=(Widget&& rhs) = default;

Widget::Widget(const Widget& rhs)              // copy ctor
: pImpl(std::make_unique(*rhs.pImpl))
{} 
Widget& Widget::operator=(const Widget& rhs)  // copy operator=
{
  *pImpl = *rhs.pImpl;
  return *this;
}

到目前为止,以上代码的实现是比较完整的了。

为了实现 Pimpl 技术,std::unique_ptr 是合适的,因为 pImpl 指针对 Impl 有独有所有权。如果你使用 std::shared_ptr 代替 std::unique_ptr,以上出现的问题将不会出现。示例如下:

// in "widget.h"
#include 
class Widget {
public:
  Widget();
  ...
private:
  struct Impl;                 
  std::shared_ptr pImpl;
};

// in "widget.cpp"
#include "widget.h"  
#include "gadget.h"
#include 
#include 

struct Widget::Impl {  // as before
  std::string name;
  std::vector data;
  Gadget g1, g2, g3;
};

Widget::Widget()
: pImpl(std::make_shared())
{}

//======================================================//
Widget w1;
auto w2(std::move(w1)); // move-construct w2
w1 = std::move(w2);     // move-assign w1

std::shared_ptr 的 deleter 不是其自身的一部分,属于控制块,我们的代码不会包含删除器的代码,因此不需要自定义析构函数,那么 move 和 copy 操作都会自定生成。而 std::shared_ptr 又是值语义的,拷贝也不会发生问题(通过引用计数进行内存管理)。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/779064.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号