Pimpl
(Pointer to implementation)不少同窗都不陌生,可是从原始指针升级到C++11的独占指针std::unique_ptr
时,会遇到一个incomplete type
的报错,本文来分析一下报错的缘由以及分享几种解决方法~c++
首先举一个传统C++中的Pimpl
的例子git
// widget.h // 预先声明 class Impl; class Widget { Impl * pImpl; };
很简单,没什么问题,可是使用的是原始指针,如今咱们升级到std::unique_ptr
github
// widget.h // 预先声明 class Impl; class Widget { std::unique_ptr<Impl> pImpl; };
很简单的一次升级,并且也能经过编译,看似也没问题,但当你建立一个Widget
的实例bash
// pimpl.cpp #include "widget.h" Widget w;
这时候,问题来了app
$ g++ pimpl.cpp In file included from /usr/include/c++/9/memory:80, from widget.h:1, from pimpl.cpp:1: /usr/include/c++/9/bits/unique_ptr.h: In instantiation of ‘void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Impl]’: /usr/include/c++/9/bits/unique_ptr.h:292:17: required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Impl; _Dp = std::default_delete<Impl>]’ 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 ‘Impl’ 79 | static_assert(sizeof(_Tp)>0, | ^~~~~~~~~~~
从报错咱们能够看出,std::unique_ptr
中须要静态检测类型的大小static_assert(sizeof(Impl)>0
,可是咱们的Impl
是一个预先声明的类型,是incomplete type
,也就无法计算,因此致使报错。函数
想要知道怎么解决,首先须要知道std::unique_ptr
为啥须要计算这个,咱们来看一下STL中相关的源码,从报错中得知是unique_ptr.h
的292行,调用了79行,咱们把先后相关源码都粘出来(来自g++ 9.3.0
中的实现)性能
// 292行附近 /// Destructor, invokes the deleter if the stored pointer is not null. ~unique_ptr() noexcept { static_assert(__is_invocable<deleter_type&, pointer>::value, "unique_ptr's deleter must be invocable with a pointer"); auto& __ptr = _M_t._M_ptr(); if (__ptr != nullptr) // 292行在这里 get_deleter()(std::move(__ptr)); __ptr = pointer(); } // 79行附近 /// 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"); // 79行在这里 static_assert(sizeof(_Tp)>0, "can't delete pointer to incomplete type"); delete __ptr; } };
std::unique_ptr
中的析构函数,调用了默认的删除器default_delete
,而default_delete
中检查了Impl
,其实就算default_delete
中不检查,到下一步delete __ptr;
,仍是会出问题,由于不完整的类型没法被delete
。ui
缘由已经知道了,那么解决方法就呼之欲出了,这里提供三种解决方法:设计
std::shared_ptr
delete pImpl
的操做,放到widget.cpp
源文件中Widget
的析构函数,但不要在widget.h
头文件中实现它其中我最推荐方法三,它不改变代码需求,且仅作一点最小的改动,下面依次分析指针
改用std::shared_ptr
// widget.h // 预先声明 class Impl; class Widget { std::shared_ptr<Impl> pImpl; };
改完就能经过编译了,这种改法最简单。可是缺点也很明显:使用shared_ptr
可能会改变项目的需求,shared_ptr
也会带来额外的性能开销,并且违反了“尽量使用unique_ptr
而不是shared_ptr
”的原则(固然这个原则是我编的,哈哈)
那为何unique_ptr
不能使用预先声明的imcomplete type
,可是shared_ptr
却能够?
由于对于unique_ptr
而言,删除器是类型的一部分:
template<typename _Tp, typename _Dp> class unique_ptr<_Tp[], _Dp>
这里的_Tp
是element_type
,_Dp
是deleter_type
而shared_ptr
却不是这样:
template<typename _Tp> class shared_ptr : public __shared_ptr<_Tp>
那为何unique_ptr
的删除器是类型的一部分,而shared_ptr
不是呢?
答案是设计如此!哈哈,说了句废话。具体来讲,删除器不是类型的一部分,使得你能够对同一种类型的shared_ptr
,使用不一样的自定义删除器
auto my_deleter = [](Impl * p) {...}; std::shared_ptr<Impl> w1(new Impl, my_deleter); std::shared_ptr<Impl> w2(new Impl); // default_deleter w1 = w2; // It's OK!
看到了么,这里的两个智能指针w1
和w2
,虽然使用了不一样的删除器,但他们是同一种类型,能够相互进行赋值等等操做。而unique_ptr
却不能这么玩
auto my_deleter = [](Impl * p) {...}; std::unique_ptr<Impl, decltype(my_deleter)> w1(new Impl, my_deleter); std::unique_ptr<Impl> w2(new Impl); // default_deleter // w1的类型是 std::unique_ptr<Impl, lambda []void (Impl *p)->void> // w2的类型是 std::unique_ptr<Impl, std::default_delete<Impl>> w1 = std::move(w2); // 错误!类型不一样,没有重载operator=
道理我都明白了,那为何要让这两种智能指针有这样的区别啊?
答案仍是设计如此!哈哈,具体来讲unique_ptr
自己就只是对原始指针的简单封装,这样作不会带来额外的性能开销。而shared_ptr
的实现提升了灵活性,但却进一步增大了性能开销。针对不一样的使用场景因此有这样的区别。
自定义删除器,将delete pImpl
的操做,放到widget.cpp
源文件中
// widget.h // 预先声明 class Impl; class Widget { struct ImplDeleter final { constexpr ImplDeleter() noexcept = default; void operator()(Impl *p) const; }; std::unique_ptr<Impl, ImplDeleter> pImpl = nullptr; };
而后在源文件widget.cpp
中
#inclued "widget.h" #include "impl.h" void Widget::ImplDeleter::operator()(Impl *p) const { delete p; }
这种方法改起来也不复杂,可是弊端也很明显,std::make_unique
无法使用了,只能本身手动new
,直接看源码吧
template<typename _Tp, typename... _Args> inline typename _MakeUniq<_Tp>::__single_object make_unique(_Args&&... __args) { return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...)); }
看出问题在哪了么?这里返回的是默认删除器类型的unique_ptr
,即std::unique_ptr<Impl, std::default_delete<Impl>>
,如方法一中所说,是不一样删除器类型的unique_ptr
是无法相互赋值的,也就是说:
pImpl = std::make_unique<Impl>(); // 错误!类型不一样,没有重载operator= pImpl = std::unique_ptr<Impl, ImplDeleter>(new Impl); // 正确!每次你都要写这么一大串
固然你也能够实现一个make_impl
,而且using
一下这个很长的类型,好比:
using unique_impl = std::unique_ptr<Impl, ImplDeleter>; template<typename... Ts> unique_impl make_impl(Ts && ...args) { return unique_impl(new Impl(std::forward<Ts>(args)...)); } // 调用 pImpl = make_impl();
看似还凑合,但总的来讲,这样作仍是感受很麻烦。而且有一个很头疼的问题:make_impl
做为函数模板,无法声明和定义分离,并且其中的用到了new
,须要完整的Impl
类型。因此,你只能把这一段模板函数写在源文件中,emmm,总感受不太对劲。
仅声明Widget
的析构函数,但不要在widget.h
头文件中实现它
// widget.h // 预先声明 class Impl; class Widget { Widget(); ~Widget(); // 仅声明 std::unique_ptr<Impl> pImpl; };
// widget.cpp #include "widget.h" #include "impl.h" Widget::Widget() : pImpl(nullptr) {} Widget::~Widget() = default; // 在这里定义
这样就解决了!是否是出乎意料的简单!而且你也能够正常的使用std::make_unique
来进行赋值。惟一的缺点就是你无法在头文件中初始化pImpl
了
但也有别的问题,由于不光是析构函数中须要析构std::unique_ptr
,还有别的也须要,好比移动构造、移动运算符等。因此在移动构造、移动运算符中,你也会遇到一样的编译错误。解决方法也很简单,同上面同样:
// widget.h // 预先声明 class Impl; class Widget { Widget(); ~Widget(); Widget(Widget && rhs); // 同析构函数,仅声明 Widget& operator=(Widget&& rhs); std::unique_ptr<Impl> pImpl; };
// widget.cpp #include "widget.h" #include "impl.h" Widget::Widget() : pImpl(nullptr) {} Widget::~Widget() = default; Widget(Widget&& rhs) = default; //在这里定义 Widget& operator=(Widget&& rhs) = default;
搞定!
本文首发于个人我的博客,欢迎你们来逛逛~~~