您的位置:首页 > 产品设计 > UI/UE

Item22 When using the Pimpl, define specific member functions in the implementation file

2016-11-30 21:43 525 查看
​   
Pimpl idiom
这个是
C++
的惯用法,我相信很多人都知道,通常为了保护我们的头文件,避免在修改类的数据成员的时候导致依赖这个头文件的程序也需要重新编译,常常被人们称为编译防火墙。例如下面这个类的头文件。

class Widget {
public:
Widget();
....
private:
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};


​​   
Gadget
是一个用户自定义类型,因此需要包含一个
gadget.h
,那么一旦
Gadget
有任何修改,那么所有包含了
widget.h
的程序都需要重新编译,如果
Widget
类是一个暴露给用户使用的类的话这就会给用户带来很大的困扰。使用
Pimpl
惯用法改造后如下:

// 头文件
class Widget {
public:
Widget();
~Widget();
...
private:
struct Impl;
Impl *pImpl;
};

// 具体实现
struct Widget::Impl {
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};

Widget::Widget() : pImpl(new Impl)
{}

Widget::~Widget()
{ delete pImpl; }


​​   看到上面使用了裸指针,在我们学会了智能指针的情况下,应该优先使用智能指针来代替裸指针,来管理内存的分配和释放。因此使用智能指针替换裸指针后的代码如下:

class Widget {
public:
Widget();
...
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
// 具体实现
struct Widget::Impl {
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};

Widget::Widget() : pImpl(std::make_unique<Impl>(new Impl))
{}


​​   经过改造后的代码去除了析构函数,真正的内存释放的操作交给了智能指针来管理了,接下来就可以把这个头文件丢给用户了,用户只需要包含这个头文件,就可以使用
Widget
了。

#include "Widget"
Widget w;       //error

error: invalid application of 'sizeof' to an incomplete type 'Widget::Impl'
note: expanded from macro 'static_assert'
typedef __static_assert_check<sizeof(__static_assert_test<(__b)>)> \
note: in instantiation of member function 'std::__1::default_delete<Widget::Impl>::operator()' requested here
__ptr_.second()(__tmp);
note: in instantiation of member function 'std::__1::unique_ptr<Widget::Impl, std::__1::default_delete<Widget::Impl> >::reset' requested here
_LIBCPP_INLINE_VISIBILITY ~unique_ptr() {reset();}
./widget.h:5:7: note: in instantiation of member function 'std::__1::unique_ptr<Widget::Impl, std::__1::default_delete<Widget::Impl> >::~unique_ptr' requested here


​   当用户包含了头文件,开始编译,出现了上面的错误,难道是
unique_ptr
不支持不完全类型(前向声明),查找相关文档发现
unique_ptr
是支持的不完全类型的。仔细看上面的错误,你会发现错误是因为调用了析构函数,在析构函数
delete
对象的时候使用了静态断言计算对象的大小。在头文件中明明没有析构相关的操作,为何会这样呢? 这一切都是
C++
在背后默默的帮我们生成的。因为上面的
widget.h
中没有声明默认的析构函数,那么C++编译器就会默默的在头文件中帮我们生成了默认析构函数,然后插入调用
unique_ptr
的析构函数的代码。最终导致出现了上面的错误,解决这个问题的方法也是很简单的,只需要将析构函数的定义放到
.cpp
文件中即可。这样在编译的时候就不会出现问题了,改造后如下:

class Widget {
public:
Widget();
~Widget();
...
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
// 具体实现

struct Widget::Impl {
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};
Widget::~Widget() = default;   // 使用了C++的default关键字
Widget::Widget() : pImpl(std::make_unique<Impl>(new Impl))
{}


​   上面的代码现在就可以正确的进行编译了,很不幸上面的代码虽然通过了编译,但是依旧不完善,因为上面的代码会导致
Widget
失去
移动语义
的能力,还记得在Item17中介绍过,当用户自定义了析构函数,会导致不会生成默认的移动构造函数和移动赋值操作符。所以上面的代码中尽管
unique_ptr
是具有移动语义的,但是因为
Widget
没有生成默认的移动构造函数,所以最终只会调用
Widget
的默认拷贝构造函数。修改起来也不难,只要主动的去声明和定义默认的移动构造和移动赋值操作符即可。

class Widget {
public:
Widget();
~Widget();
Widget(Widget&& rhs) = default;
Widget& operator=(Widget&& rhs) = default;
...
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
// 用户代码
#include "widget.h"
int main() {
Widget w;
Widget w1(std::move(w));  // error
}
In instantiation of ‘void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Widget::Impl]’:
required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Widget::Impl; _Dp = std::default_delete<Widget::Impl>]’
widget.h:9:3:   required from here
invalid application of ‘sizeof’ to incomplete type ‘Widget::Impl’
static_assert(sizeof(_Tp)>0,
^


​   我们的代码正在一步步的完善,但是很可惜上面的代码居然编译没有通过,根据错误信息可以知道的是,编译失败的原因应该也是因为在头文件中包含了
unique_prt
析构相关的代码,导致在调用默认的删除器
delete
的时候进行了静态断言并失败,这是因为移动构造在遇到异常的时候会调用了
unique_ptr
的析构。问题既然找到了,解决这个问题也是很简单的,直接将移动语义相关的实现从头文件移除即可。

class Widget {
public:
Widget();
~Widget();
Widget(Widget&& rhs); //具体实现放到.cpp文件中
Widget& operator=(Widget&& rhs); //具体实现放到.cpp文件中

private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};


​​   到此为止,上面的代码终于可以编译通过,并且正常运行了,但是因为我们使用的是
std::unique_ptr
,这会存在一个问题,对于
Widget
的拷贝是一个浅拷贝,因为
uniqe_ptr
是一个所有权独享的对象,对它进行拷贝会转移所有权。要想实现深拷贝,就不能依赖默认的拷贝构造函数,需要自己自定义,代码如下:

Widget::Widget(const Widget& rhs) : pImpl(std::make_unique<Impl>(*rhs.pImpl)) {
}

Widget& Widget::operator=(const Widget& rhs) {
*pImpl = *rhs.pImpl;
return *this;
}


​​   如果你感兴趣的话,你完全可以把
std::uniqe_ptr
换成
std::shared_ptr
,与此同时你会惊奇的发现,上文中提到的诸多限制(要自定义析构函数,要自定义移动操作等等),在使用
shared_ptr
的场景下是不存在的。

// widget.h
class Widget {
public:
Widget();
.....
private:
struct Impl;
std::shared_ptr<Impl> pImpl;
};

// widget.cc
struct Widget::Impl {
std::string name;
std::vector<double> data;
};

Widget::Widget() : pImpl(std::make_shared<Impl>())
{}

// 用户代码
#include "widget.h"
int main() {
Widget client;
Widget b(std::move(client));
return 0;
}


​​   上面的代码可以正常编译通过,并且具备移动语义,深拷贝,看起来是不是很神奇,几乎一样的代码居然差距如此之大,下面具体来分析一下导致这样的结果的本质原因。还记得在Item19Item18 对着两个智能指针的介绍吗?,删除器是属于unique_ptr的一部分的,通过它的源代码也可以看出这一点。

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

/** @brief Converting constructor.
*
* Allows conversion from a deleter for arrays of another type, @p _Up,
* only if @p _Up* is convertible to @p _Tp*.
*/
template<typename _Up, typename = typename
enable_if<is_convertible<_Up*, _Tp*>::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;
}
};


​​   这也就是导致问题的最主要的原因了,头文件中包含了
unique_ptr
的相关头文件,很自然,关于删除器相关的代码也都被包含进去了,可以看到上面代码中的
static_assrt
它就是引起编译错误的罪魁祸首,如果使用
shared_ptr
的话那就不一样了,在前面的文章中其实也介绍过,删除器并不是
shared_ptr
本身所包含的,而是包含在控制块中,
shared_ptr
通过指针指向这个控制块,所以当你使用
shared_ptr
的时候自然就不会包含删除器相关的代码了,也就不需要自定义析构函数,来解决编译的问题,没有自定义析构函数,默认的移动构造函数就会生效,自然就不需要显示的声明移动构造函数了,最后,
shared_ptr
本身其实是值语义,因此对于拷贝来说自然是没啥问题了。不会像
unique_ptr
那样出现所有权转移的问题。

​​   上文中说了很多,似乎让人觉得在这里应该使用
shared_ptr
替换
unique_ptr
,因为至少从上文中可以看出
shared_ptr
相比于
unique_ptr
还是很方便的。但是使用
shared_ptr
会导致生成大量汇编代码,速度上相比于
unique_ptr
也不客观,
unique_ptr
则更加轻量级,在不自定义删除器的情况下大小和裸指针是一样的,通过
unique_ptr
访问对象的成员的开销和裸指针也是一样的,几乎没有太多的开销,所以在
pimpl
的惯用法下,还是鼓励使用
unique_ptr
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐