319
技術社區[雲棲]
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作為對象的成員一般會有哪些額外開銷?
- 通常要存起來,多占用空間。
- 調用時可能會有一次額外的跳轉(相比
delete
或delete[]
)。
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需要是模板類型的一部分。
相關文檔
- Why does unique_ptr take two template parameters when shared_ptr only takes one?
- Why does unique_ptr have the deleter as a type parameter while shared_ptr doesn't?
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模板參數的含義是:
-
_T1
和_T2
在去掉頂層的const
和volatile
後,是不是相同類型。 -
_T1
是不是空類型。 -
_T2
是不是空類型。
滿足以下條件的類型就是空類型:
- 不是union;
- 除了size為0的位域之外,沒有非static的成員變量;
- 沒有虛函數;
- 沒有虛基類;
- 沒有非空的基類。
可以看到,在_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的,它在調用上的開銷也被省掉了!
遺留問題
-
__libcpp_compressed_pair_switch
在_T1
和_T2
類型相同,且都是空類型時,為什麼隻繼承自_T1
,而把_T2
作為成員變量的類型? -
unique_ptr
與pointer_traits
是如何交互的?
最後更新:2017-11-10 14:35:27