• 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是如何交互的?
  • 相关阅读:
    MyBatis学习之简单增删改查操作、MyBatis存储过程、MyBatis分页、MyBatis一对一、MyBatis一对多
    最简单的mybatis增删改查样例
    JAVA地址栏重写很详细
    rapid-generator JAVA代码生成器
    PouchDB:可随时同步的开源JavaScript数据库
    写给java web一年左右工作经验的人
    使用 XMLBeans 进行编程
    使用Spring JMS轻松实现异步消息传递
    Spring JMSTemplate 与 JMS 原生API比较
    浅析深究什么是中间件
  • 原文地址:https://www.cnblogs.com/fuzhe1989/p/7763623.html
Copyright © 2020-2023  润新知