std::unique_ptr使用incomplete type的報錯分析和解決

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_ptrgithub

// 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;,仍是會出問題,由於不完整的類型沒法被deleteui

解決方法

緣由已經知道了,那麼解決方法就呼之欲出了,這裏提供三種解決方法:設計

  • 方法一:改用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>

這裏的_Tpelement_type_Dpdeleter_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!

看到了麼,這裏的兩個智能指針w1w2,雖然使用了不一樣的刪除器,但他們是同一種類型,能夠相互進行賦值等等操做。而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;

搞定!

參考資料

本文首發於個人我的博客,歡迎你們來逛逛~~~

原文地址: std::unique_ptr使用incomplete type的報錯分析和解決 | 肝!

相關文章
相關標籤/搜索