Pimpl
(Pointer to implementation)很多同學都不陌生,但是從原始指針升級到C++11的獨占指針std::unique_ptr
時,會遇到一個incomplete type
的報錯,本文來分析一下報錯的原因以及分享幾種解決方法~
問題現象
首先舉一個傳統C++中的Pimpl
的例子
// widget.h
// 預先聲明
class Impl;
class Widget
{
Impl * pImpl;
};
很簡單,沒什么問題,但是使用的是原始指針,現在我們升級到std::unique_ptr
// widget.h
// 預先聲明
class Impl;
class Widget
{
std::unique_ptr<Impl> pImpl;
};
很簡單的一次升級,而且也能通過編譯,看似也沒問題,但當你創建一個Widget
的實例
// pimpl.cpp
#include "widget.h"
Widget w;
這時候,問題來了
$ 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
。
解決方法
原因已經知道了,那么解決方法就呼之欲出了,這里提供三種解決方法:
- 方法一:改用
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
中
#include "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;
搞定!
參考資料
- 當使用Pimpl慣用法,請在實現文件中定義特殊成員函數 -
《Effective Modern Cpp》 - std::unique_ptr with an incomplete type won't compile - Stack Overflow
本文首發於我的個人博客,歡迎大家來逛逛~~~