Item22 When using the Pimpl, define specific member functions in the implementation file
2016-11-30 21:43
525 查看
看到上面使用了裸指针,在我们学会了智能指针的情况下,应该优先使用智能指针来代替裸指针,来管理内存的分配和释放。因此使用智能指针替换裸指针后的代码如下:
经过改造后的代码去除了析构函数,真正的内存释放的操作交给了智能指针来管理了,接下来就可以把这个头文件丢给用户了,用户只需要包含这个头文件,就可以使用
当用户包含了头文件,开始编译,出现了上面的错误,难道是
上面的代码现在就可以正确的进行编译了,很不幸上面的代码虽然通过了编译,但是依旧不完善,因为上面的代码会导致
我们的代码正在一步步的完善,但是很可惜上面的代码居然编译没有通过,根据错误信息可以知道的是,编译失败的原因应该也是因为在头文件中包含了
到此为止,上面的代码终于可以编译通过,并且正常运行了,但是因为我们使用的是
如果你感兴趣的话,你完全可以把
上面的代码可以正常编译通过,并且具备移动语义,深拷贝,看起来是不是很神奇,几乎一样的代码居然差距如此之大,下面具体来分析一下导致这样的结果的本质原因。还记得在Item19和Item18 对着两个智能指针的介绍吗?,删除器是属于unique_ptr的一部分的,通过它的源代码也可以看出这一点。
这也就是导致问题的最主要的原因了,头文件中包含了
上文中说了很多,似乎让人觉得在这里应该使用
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; }
上面的代码可以正常编译通过,并且具备移动语义,深拷贝,看起来是不是很神奇,几乎一样的代码居然差距如此之大,下面具体来分析一下导致这样的结果的本质原因。还记得在Item19和Item18 对着两个智能指针的介绍吗?,删除器是属于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。
相关文章推荐
- Item 46: Define non-member functions inside templates when type conversions are desired(Effective C++)
- Using the itemDoubleClick event to open nodes in a Flex Tree control
- Saving files locally using the FileReference class’s save() method in Flash Player 10
- Adding Custom Actions to the List Item Menu in SharePoint 2010 Using SharePoint Designer 2010[Sharepoint中添加自定义菜单]
- Resolution to the record count increasing of the file exported from DB when ‘0A’ is included in it
- Read A Flat File Using File Layout And Insert Into The Specific Component Using CI ( Bulk Insert )
- Alternating row colors in a Flex Tree control using the alternatingItemColors style
- BI Java 补丁错误处理 :Cannot login to the SAP J2EE Engine using user and password as provided in the Filesystem Secure Store. Enter va
- Using WinInet functions to download a file asynchronously in Delphi
- Resolution to the record count increasing of the file exported from DB when ‘0A’ is included in it
- Uploading files in Flex using the FileReference class
- Opening nodes in a Flex Tree control using the expandItem() method
- How to solve the linking errors when using the ATOMIC functions
- Item 24: Declare non-member functions when type conversions should apply to all parameters(Effective C++)
- Question 5: Which of the following statements regarding the benefits of using template functions over preprocessor #define macro
- Using the Network Functions in C# (Part I - User Functions)
- How to Modify the web.config file in SharePoint using SPWebConfigModification?
- Previewing an image before uploading it using the FileReference class in Flash Player 10
- You cannot access the client's file system using the FileSystemObject in your ASP code
- How to solve: when using tab in gnomeTerminal , the entire screen flash,