閱讀319 返回首頁    go 技術社區[雲棲]


C++:為什麼unique_ptr的Deleter是模板類型參數,而shared_ptr的Deleter不是?

為什麼unique_ptr的Deleter是模板類型參數,而shared_ptr的Deleter不是?

template <class T, class D = default_delete<T>>
class unique_ptr {
public:
    ...
    unique_ptr (pointer p,
        typename conditional<is_reference<D>::value,D,const D&> del) noexcept;
    ...
};

template <class T> 
class shared_ptr {
public:
    ...
    template <class U, class D> 
    shared_ptr (U* p, D del);
    ...
};

上麵的代碼中能看到unique_ptr的第二個模板類型參數是Deleter,而shared_ptr的Delete則隻是構造函數參數的一部分,並不是shared_ptr的類型的一部分。

為什麼會有這個區別呢?

答案是效率。unique_ptr的設計目標之一是盡可能的高效,如果用戶不指定Deleter,就要像原生指針一樣高效。

Deleter作為對象的成員一般會有哪些額外開銷?

  1. 通常要存起來,多占用空間。
  2. 調用時可能會有一次額外的跳轉(相比deletedelete[])。

shared_ptr總是要分配一個ControlBlock的,多加一個Deleter的空間開銷也不大,第一條pass;shared_ptr在析構時要先原子減RefCount,如果WeakCount也為0還要再析構ControlBlock,那麼調用Deleter析構持有的對象時多一次跳轉也不算什麼,第二條pass。

既然shared_ptr並不擔心Deleter帶來的額外開銷,同時把Deleter作為模板類型的一部分還會導致使用上變複雜,那麼它隻把Deleter作為構造函數的類型就是顯然的事情了。

unique_ptr采用了“空基類”的技巧,將Deleter作為基類,在用戶不指定Deleter時根本不占空間,第一條pass;用戶不指定Deleter時默認的Deleter會是default_delete,它的operator()在類的定義內,會被inline掉,這樣調用Deleter時也就沒有額外的開銷了,第二條pass。

因此unique_ptr通過上麵兩個技巧,成功的消除了默認Deleter可能帶來的額外開銷,保證了與原生指針完全相同的性能。代價就是Deleter需要是模板類型的一部分。

相關文檔

unique_ptr是如何使用空基類技巧的

我們參考clang的實現來學習一下unique_ptr使用的技巧。

template <class _Tp, class _Dp = default_delete<_Tp> >
class unique_ptr
{
public:
    typedef _Tp element_type;
    typedef _Dp deleter_type;
    typedef typename __pointer_type<_Tp, deleter_type>::type pointer;
private:
    __compressed_pair<pointer, deleter_type> __ptr_;
    ...
};

忽略掉unique_ptr中的各種成員函數,我們看到它隻有一個成員變量__ptr__,類型是__compressed_pair<pointer, deleter_type>。我們看看它是什麼,是怎麼省掉了Deleter的空間的。

template <class _T1, class _T2>
class __compressed_pair
    : private __libcpp_compressed_pair_imp<_T1, _T2> {
    ...
};

__compressed_pair沒有任何的成員變量,就說明它的秘密藏在了它的基類中,我們繼續看。

template <class _T1, class _T2, unsigned = __libcpp_compressed_pair_switch<_T1, _T2>::value>
class __libcpp_compressed_pair_imp;

__libcpp_compressed_pair_imp有三個模板類型參數,前兩個是傳入的_T1_T2,第三個參數是一個無符號整數,它是什麼?我們往下看,看到了它的若幹個特化版本:

template <class _T1, class _T2>
class __libcpp_compressed_pair_imp<_T1, _T2, 0>
{
private:
    _T1 __first_;
    _T2 __second_;
    ...
};

template <class _T1, class _T2>
class __libcpp_compressed_pair_imp<_T1, _T2, 1>
    : private _T1
{
private:
    _T2 __second_;
    ...
};

template <class _T1, class _T2>
class __libcpp_compressed_pair_imp<_T1, _T2, 2>
    : private _T2
{
private:
    _T1 __first_;
    ...
};

template <class _T1, class _T2>
class __libcpp_compressed_pair_imp<_T1, _T2, 3>
    : private _T1,
      private _T2
{
    ...
};

看起來第三個參數有4種取值,分別是:

  • 0: 沒有基類,兩個成員變量。
  • 1: 有一個基類_T1,和一個_T2類型的成員變量。
  • 2: 有一個基類_T2,和一個_T1類型的成員變量。
  • 3: 有兩個基類_T1_T2,沒有成員變量。

__compressed_pair繼承自__libcpp_compressed_pair_imp<_T1, _T2>,沒有指定第三個參數的值,那麼這個值應該來自__libcpp_compressed_pair_switch<_T1, _T2>::value。我們看一下__libcpp_compressed_pair_switch是什麼:

template <class _T1, class _T2, bool = is_same<typename remove_cv<_T1>::type,
                                                     typename remove_cv<_T2>::type>::value,
                                bool = is_empty<_T1>::value
                                       && !__libcpp_is_final<_T1>::value,
                                bool = is_empty<_T2>::value
                                       && !__libcpp_is_final<_T2>::value
         >
struct __libcpp_compressed_pair_switch;

template <class _T1, class _T2, bool IsSame>
struct __libcpp_compressed_pair_switch<_T1, _T2, IsSame, false, false> {enum {value = 0};};

template <class _T1, class _T2, bool IsSame>
struct __libcpp_compressed_pair_switch<_T1, _T2, IsSame, true, false>  {enum {value = 1};};

template <class _T1, class _T2, bool IsSame>
struct __libcpp_compressed_pair_switch<_T1, _T2, IsSame, false, true>  {enum {value = 2};};

template <class _T1, class _T2>
struct __libcpp_compressed_pair_switch<_T1, _T2, false, true, true>    {enum {value = 3};};

template <class _T1, class _T2>
struct __libcpp_compressed_pair_switch<_T1, _T2, true, true, true>     {enum {value = 1};};

__libcpp_compressed_pair_switch的三個bool模板參數的含義是:

  1. _T1_T2在去掉頂層的constvolatile後,是不是相同類型。
  2. _T1是不是空類型。
  3. _T2是不是空類型。

滿足以下條件的類型就是空類型:

  1. 不是union;
  2. 除了size為0的位域之外,沒有非static的成員變量;
  3. 沒有虛函數;
  4. 沒有虛基類;
  5. 沒有非空的基類。

可以看到,在_T1_T2不同時,它們中的空類型就會被當作__compressed_pair的基類,就會利用到C++中的“空基類優化“。

那麼在unique_ptr中,_T1_T2都是什麼呢?看前麵的代碼,_T1就是__pointer_type<_Tp, deleter_type>::type,而_T2則是Deleter,在默認情況下是default_delete<_Tp>

我們先看__pointer_type是什麼:

namespace __pointer_type_imp
{

template <class _Tp, class _Dp, bool = __has_pointer_type<_Dp>::value>
struct __pointer_type
{
    typedef typename _Dp::pointer type;
};

template <class _Tp, class _Dp>
struct __pointer_type<_Tp, _Dp, false>
{
    typedef _Tp* type;
};

}  // __pointer_type_imp

template <class _Tp, class _Dp>
struct __pointer_type
{
    typedef typename __pointer_type_imp::__pointer_type<_Tp, typename remove_reference<_Dp>::type>::type type;
};

可以看到__pointer_type<_Tp, deleter_type>::type就是__pointer_type_imp::__pointer_type<_Tp, typename remove_reference<_Dp>::type>::type。這裏我們看到了__has_pointer_type,它是什麼?

namespace __has_pointer_type_imp
{
    template <class _Up> static __two __test(...);
    template <class _Up> static char __test(typename _Up::pointer* = 0);
}

簡單來說__has_pointer_type就是:如果_Up有一個內部類型pointer,即_Up::pointer是一個類型,那麼__has_pointer_type就返回true,例如pointer_traits::pointer,否則返回false

大多數場景下_Dp不會是pointer_traits,因此__has_pointer_type就是false__pointer_type<_Tp, deleter_type>::type就是_Tp*,我們終於看到熟悉的原生指針了!

_T1是什麼我們已經清楚了,就是_Tp*,它不會是空基類。那麼_T2呢?我們看default_delete<_Tp>

template <class _Tp>
struct default_delete
{
    template <class _Up>
        default_delete(const default_delete<_Up>&,
             typename enable_if<is_convertible<_Up, _Tp>::value>::type* = 0) _NOEXCEPT {}
    void operator() (_Tp* __ptr) const _NOEXCEPT
        {
            static_assert(sizeof(_Tp) > 0, "default_delete can not delete incomplete type");
            static_assert(!is_void<_Tp>::value, "default_delete can not delete incomplete type");
            delete __ptr;
        }
};

我們看到default_delete符合上麵說的空類型的幾個要求,因此_T2就是空類型,也是__compressed_pair的基類,在”空基類優化“後,_T2就完全不占空間了,隻占一個原生指針的空間。

而且default_delete::operator()是定義在default_delete內部的,默認是inline的,它在調用上的開銷也被省掉了!

遺留問題

  1. __libcpp_compressed_pair_switch_T1_T2類型相同,且都是空類型時,為什麼隻繼承自_T1,而把_T2作為成員變量的類型?
  2. unique_ptrpointer_traits是如何交互的?

最後更新:2017-11-10 14:35:27

  上一篇:go  如何成功地實現混合雲應用集成
  下一篇:go  C++:delete不完整類型的指針