zoukankan      html  css  js  c++  java
  • 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是如何交互的?
  • 相关阅读:
    《玩转.NET Micro Framework 移植基于STM32F10x处理器》内容介绍
    《玩转.NET Micro Framework 移植基于STM32F10x处理器》前言
    《玩转.NET Micro Framework 移植基于STM32F10x处理器》内容介绍
    《玩转.NET Micro Framework 移植基于STM32F10x处理器》微软中国.NET Micro Framework项目组工程师所作之序
    《玩转.NET Micro Framework 移植基于STM32F10x处理器》资源汇总
    《玩转.NET Micro Framework 移植基于STM32F10x处理器》微软中国.NET Micro Framework项目组工程师所作之序
    《玩转.NET Micro Framework 移植基于STM32F10x处理器》前言
    Windows、Linux、ARM、Android、iOS全平台支持的RTMP推流组件libEasyRTMP库接口调用说明
    简单高效易用Windows/Linux/ARM/Android/iOS平台实现RTMP推送组件EasyRTMPAndroid MediaCodec硬编码流程介绍
    RTSP网络监控摄像头如何实现Windows、Linux、ARM、Android、iOS全平台支持的拉RTSP流推出RTMP直播流?
  • 原文地址:https://www.cnblogs.com/fuzhe1989/p/7763623.html
Copyright © 2011-2022 走看看