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;
搞定!
本文首發於個人我的博客,歡迎你們來逛逛~~~