std::unique_ptr & std::make_unique

Published: April 28 2014

今天继续来一弹,这次是比较简单的 unqiue_ptr。 用来持有 ownership 的指针。unique 顾名思义,不可以像 shared 随便来拿。

c++11 的 rvalue ref 和 move 无疑是让 unique_ptr 方便了许多,当然没有他们的时候, unqiue_ptr 也是可以被模拟出来的

不过先不管这些,直接来看新标准新代码。还是 libstdc++v3。为了简单起见,这里只说最简单的 unique_ptr,不看 数组版的 unique_ptr。

include/bits/unique_ptr.h line 128

  /// 20.7.1.2 unique_ptr for single objects.
  template <typename _Tp, typename _Dp = default_delete<_Tp> >
    class unique_ptr

unqiue_ptr 接受两个模板参数,一个就是 ptr 所指类型,另一个是 deleter,也就是 release 要在 ptr 上调用的类似析构一样的函数。

索性先看一下 default_delete 是怎样的?其实随便 yy 也想的到,这应该就是 delete 包装了一下。

  /// 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");
        static_assert(sizeof(_Tp)>0,
                      "can't delete pointer to incomplete type");
        delete __ptr;
      }
    };

似乎有奇奇怪怪的东西混了进来。抛开 private 中的 trait 先不看,先来下面的构造函数。

constexpr 的 default 构造函数,关于 constexpr http://en.cppreference.com/w/cpp/language/constexpr。 constexpr,noexcept 这些都是为了尽量给编译器更多的优化空间。

下面的构造函数

enable_if<is_convertible<_Up*, _Tp*>::value>::type

类似的方法之前有见过。也就是在 _Up 可以被转换成 _Tp 的时候,提供这个从 _Up deleter 到 _Tp deleter 的构造。 可以从 default_delete<Derived> 构造 default_delete<Base>

下面的 operator 就是干活的啦。里面就是 delete。当然还有 static_assert 来做检查类型的完整性。

default_delete 和 unique_ptr 一样,下面有一个数组版的,略过。我们接着来看 unique_ptr。

  /// 20.7.1.2 unique_ptr for single objects.
  template <typename _Tp, typename _Dp = default_delete<_Tp> >
    class unique_ptr
    {
      // use SFINAE to determine whether _Del::pointer exists
      class _Pointer
      {
        template<typename _Up>
          static typename _Up::pointer __test(typename _Up::pointer*);
        template<typename _Up>
          static _Tp* __test(...);
        typedef typename remove_reference<_Dp>::type _Del;
      public:
        typedef decltype(__test<_Del>(0)) type;
      };
      typedef std::tuple<typename _Pointer::type, _Dp> __tuple_type;
      __tuple_type _M_t;

可能看到这里有点困惑, 为什么要 deleter::type::pointer? 这就要看 unqiue_ptr 的说明了

pointer std::remove_reference<Deleter>::type::pointer if that type exists, otherwise T*

也就是说,unqiue_ptr 里面实际 hold 的 pointer 是首先选择 deleter::type::pointer ,如果不存在则用 T*。 这里再次用 SFINAE 来决定 deleter::type::pointer 是否存在。

注意看 _Pointer::type。

typedef decltype(__test<_Del>(0)) type;

再看 __test 函数的重载,就一目了然了。巧妙的利用了重载函数的返回值来确定 pointer 类型。

继续看下面 unqiue_ptr 的成员。

      typedef std::tuple<typename _Pointer::type, _Dp> __tuple_type;
      __tuple_type _M_t;

原来 unqiue_ptr 里面把实际的指针和deleter结构封成了一个 tuple,作为成员。这也是 unqiue_ptr 唯一的成员。 咦,为什么要这样做呢?直接做成两个成员不是也可以。继续往下,看看能不能找到答案。

    public:
      typedef typename _Pointer::type pointer;
      typedef _Tp element_type;
      typedef _Dp deleter_type;
      // Constructors.
      constexpr unique_ptr() noexcept
      : _M_t()
      { static_assert(!is_pointer<deleter_type>::value,
                     "constructed with null function pointer deleter"); }
      explicit
      unique_ptr(pointer __p) noexcept
      : _M_t(__p, deleter_type())
      { static_assert(!is_pointer<deleter_type>::value,
                     "constructed with null function pointer deleter"); }

      unique_ptr(pointer __p,
          typename conditional<is_reference<deleter_type>::value,
            deleter_type, const deleter_type&>::type __d) noexcept
      : _M_t(__p, __d) { }

      unique_ptr(pointer __p,
          typename remove_reference<deleter_type>::type&& __d) noexcept
      : _M_t(std::move(__p), std::move(__d))
      { static_assert(!std::is_reference<deleter_type>::value,
                      "rvalue deleter bound to reference"); }

都是很正常的构造,static_assert 对模板 deleter 做了防范。如果 deleter 就是一个函数指针的话,不可以做这种默认构造(deleter 都是 null 你想死?)。 后面的 static reference 是在防范在 deleter_type 是一个 reference 的时候拿到了 rvalue,引用临时量就挂了~~

void dl(int *a) {
    delete a;
}
unique_ptr<int, void(*)(int*)> a(new int(1));   // No
unique_ptr<int, void(*)(int*)> a(new int(1), &dl);   // Yes

接下来还有 nullptr_t。 nullptr_t 是 nullptr 的 type,stl 的指针类似物构造都对 nullptr_t 做了处理,算是一个小优化。c++11 首选 nullptr 喔。

      /// Creates a unique_ptr that owns nothing.
      constexpr unique_ptr(nullptr_t) noexcept : unique_ptr() { }

还有 move ctor

      /// Move constructor.
      unique_ptr(unique_ptr&& __u) noexcept
      : _M_t(__u.release(), std::forward<deleter_type>(__u.get_deleter())) { }

也就是 a(unqiue_ptr<Type>(….)) 的时候,会把 b 的 pointer release 下来拿在自己手里,还会拿到对方的 deleter。 既然看到了 release,直接跳到后面看一眼 release。

      pointer
      release() noexcept
      {
        pointer __p = get();
        std::get<0>(_M_t) = pointer();
        return __p;
      }

顺便看下 get

      pointer
      get() const noexcept
      { return std::get<0>(_M_t); }

并没有什么特别的。

回到刚才的点,接着看构造。

      template<typename _Up, typename _Ep, typename = _Require<
               is_convertible<typename unique_ptr<_Up, _Ep>::pointer, pointer>,
               __not_<is_array<_Up>>,
               typename conditional<is_reference<_Dp>::value,
                                    is_same<_Ep, _Dp>,
                                    is_convertible<_Ep, _Dp>>::type>>
        unique_ptr(unique_ptr<_Up, _Ep>&& __u) noexcept
        : _M_t(__u.release(), std::forward<_Ep>(__u.get_deleter()))
        { }

这里还是用模板 trait,为了处理 unqiue_ptr<Base>(new Derived) 这种情况。

下面是 dtor

      /// Destructor, invokes the deleter if the stored pointer is not null.
      ~unique_ptr() noexcept
      {
        auto& __ptr = std::get<0>(_M_t);
        if (__ptr != nullptr)
          get_deleter()(__ptr);
        __ptr = pointer();
      }

先拿到指针后,调用 deleter 进行析构,然后把指针清 0.

后面则是 operator=(&&) ,处理 a = std::move(b)。

      unique_ptr&
      operator=(unique_ptr&& __u) noexcept
      {
        reset(__u.release());
        get_deleter() = std::forward<deleter_type>(__u.get_deleter());
        return *this;
      }

其中的 reset 是先做 swap,再把原来的 pointer 上掉 deleter。

      void
      reset(pointer __p = pointer()) noexcept
      {
        using std::swap;
        swap(std::get<0>(_M_t), __p);
        if (__p != pointer())
          get_deleter()(__p);
      }

后面还有 Derived class, nullptr 情况的 operator=(&&),就不一一说明了,跟之前都相似。

还有平淡无奇的 operator* 和 operator->

      /// Dereference the stored pointer.
      typename add_lvalue_reference<element_type>::type
      operator*() const
      {
        _GLIBCXX_DEBUG_ASSERT(get() != pointer());
        return *get();
      }
      /// Return the stored pointer.
      pointer
      operator->() const noexcept
      {
        _GLIBCXX_DEBUG_ASSERT(get() != pointer());
        return get();
      }

不过话说回来,unique_ptr 的操作符重载和各种构造函数真是够全的,数组版的 unique_ptr 的还有 operator[]。 可以拿来当模板学习。

注意到 typename add_lvalue_reference<element_type>::type,其实这里直接上 element_type& 也可以的。

后面还有 operator bool,让你尽情的 if。

      /// Return @c true if the stored pointer is not null.
      explicit operator bool() const noexcept
      { return get() == pointer() ? false : true; }

, 最后收尾的是 swap,还有把拷贝构造和 operator= 干掉了。

      /// Exchange the pointer and deleter with another object.
      void
      swap(unique_ptr& __u) noexcept
      {
        using std::swap;
        swap(_M_t, __u._M_t);
      }
      // Disable copy from lvalue.
      unique_ptr(const unique_ptr&) = delete;
      unique_ptr& operator=(const unique_ptr&) = delete;

当然后面还有各种 operator== 等等。 一路看下来,普普通通,比较核心的部分就是 && 和 move 这些东西。 对了之前还有一个问题没解决,就是为什么用 tuple 封装一下。感觉就做成成员完全没问题啊。。

我们换个方向,看看其他 std 也是这么实现的么?看看 clang 的 libcxx。

include/memory line 2456

template <class _Tp, class _Dp = default_delete<_Tp> >
class _LIBCPP_TYPE_VIS_ONLY 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_;

libcxx 的实现没用 tuple,而是用了 __compress_pair,我们追上去看看。可能熟悉 boost 的人看到这里已经知道他在干嘛了,= = 但是我还不知道啊。。。。

line 2300

template <class _T1, class _T2>
class __compressed_pair
    : private __libcpp_compressed_pair_imp<_T1, _T2>

原来是在外面包了一层。跟上去,发现问题有一些复杂

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

template <class _T1, class _T2>
class __libcpp_compressed_pair_imp<_T1, _T2, 0>

template <class _T1, class _T2>
class __libcpp_compressed_pair_imp<_T1, _T2, 1>
    : private _T1

template <class _T1, class _T2>
class __libcpp_compressed_pair_imp<_T1, _T2, 2>
    : private _T2

template <class _T1, class _T2>
class __libcpp_compressed_pair_imp<_T1, _T2, 3>
    : private _T1 , private _T2

竟然用了 private 继承,这又是为什么呢?为何不直接将 T1, T2 做成成员? 看一下 __libcpp_compressed_pair_switch。通过这个 trait,在编译期的时候会根据 T1, T2 的类型选择对应的 pair_impl。

line 1905

template <class _T1, class _T2, bool = is_same<typename remove_cv<_T1>::type,
                                                     typename remove_cv<_T2>::type>::value,
                                bool = is_empty<_T1>::value
#if __has_feature(is_final)
                                       && !__is_final(_T1)
#endif
                                ,
                                bool = is_empty<_T2>::value
#if __has_feature(is_final)
                                       && !__is_final(_T2)
#endif
         >
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};};
template <class _T1, class _T2, unsigned = __libcpp_compressed_pair_switch<_T1, _T2>::value>
class __libcpp_compressed_pair_imp;

事情到这里就清晰了。在 T1 和 T2 都是 is_empty(http://en.cppreference.com/w/cpp/types/is_empty) 且 不 is_final(是否可继承) 时候就会选择进行 private 继承的实现,而如果其中一方不是的话,则会根据另一方的情况,尽量选择 private 继承的实现。

C++ 竟然都要有 final 了,http://en.cppreference.com/w/cpp/language/final。 再联想一下 override 的出现,这是逐渐向 C# 接轨的节奏么?

看到这里,事情似乎变得更复杂了,这个 compress pair 总是想尽量 private inherit T1 和 T2。私有继承有什么作用呢?为什么这个叫做 compressed_pair?

empty base optimization http://en.cppreference.com/w/cpp/language/ebo。 这就是问题的答案。C++ 中, sizeof(anytype) > 0,就算一个空的 struct 也会 sizeof(emptyStruct) == 1。而 deleter 经常是一个带 operator() 没有成员的 struct,而作为成员的话,总会多出来这个 1 的大小,但是如果是 private 继承,编译器会把 empty base class 这个空间优化掉~~~ 这就是用 compressed_pair 的目的。

注意到 [quote] Empty base optimization is prohibited if one of the empty base classes is also the type of the first non-static data member, or the base of the type of the first non-static data member since the two base subobjects have the same type, and therefore are required to have different addresses within the object representation of the most derived type. [/quote]

所以类型相同的时候,会只对其中一个 做 private 继承。

到这里,明白了 compress_pair 的作用以及为什么 libcxx 里面会用 compressed_pair 对 pointer 和 deleter 进行包装。回过头,libstdc++ 用 tuple 是何解呢?不妨看一下 tuple 是怎么实现的。

include/tr1/tuple

  template<int _Idx, typename... _Elements>
    struct _Tuple_impl;
  /**
   * Zero-element tuple implementation. This is the basis case for the
   * inheritance recursion.
   */
  template<int _Idx>
    struct _Tuple_impl<_Idx> { };
  /**
   * Recursive tuple implementation. Here we store the @c Head element
   * and derive from a @c Tuple_impl containing the remaining elements
   * (which contains the @c Tail).
   */
  template<int _Idx, typename _Head, typename... _Tail>
    struct _Tuple_impl<_Idx, _Head, _Tail...>
    : public _Tuple_impl<_Idx + 1, _Tail...>

其意不言自明。

then, make_unique make_unique 也有单机版和数组版两种。

    auto unique_var = make_unique<int>(3);
    auto unique_array = make_unique<int[]>(3);

分别是一个 3,和一个长度为 3 的动态数组。 然而 make_unique 是不允许构造定长数组的(不能匹配到 int[N] 上)

auto err = make_unique<int[3]>(); // error!

实现分为两部分 首先是 _MakeUniq 结构

#if __cplusplus > 201103L
  template<typename _Tp>
    struct _MakeUniq
    { typedef unique_ptr<_Tp> __single_object; };
  template<typename _Tp>
    struct _MakeUniq<_Tp[]>
    { typedef unique_ptr<_Tp[]> __array; };
  template<typename _Tp, size_t _Bound>
    struct _MakeUniq<_Tp[_Bound]>
    { struct __invalid_type { }; };
  /// std::make_unique for single objects

然后是 make_unique 函数,几个重载。

  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)...)); }
  /// std::make_unique for arrays of unknown bound
  template<typename _Tp>
    inline typename _MakeUniq<_Tp>::__array
    make_unique(size_t __num)
    { return unique_ptr<_Tp>(new typename remove_extent<_Tp>::type[__num]()); }
  /// Disable std::make_unique for arrays of known bound
  template<typename _Tp, typename... _Args>
    inline typename _MakeUniq<_Tp>::__invalid_type
    make_unique(_Args&&...) = delete;
#endif

_MakeUnqi<_Tp> 是被拿来做返回类型匹配的 helper。匹配优先级如下: 1. 在 make_unique<T[N]> 时, struct _MakeUniq<_Tp[_Bound]> 匹配,得到 struct __invalid_type { }; ,而此时的函数 =delete,被干掉了。 2. 在 make_unique<T[]> 时, struct _MakeUniq<_Tp[]> 匹配,得到 typedef unique_ptr<_Tp[]> __array; 3. 在 make_unique<T> 时,struct _MakeUniq<_Tp> 匹配,得到 typedef unique_ptr<_Tp> __single_object;

注意到这些模板函数签名在某些情况下可以相同的

 auto unique_var = make_unique<size_t>();  // single vs. invalid
 auto unique_var = make_unique<size_t>(1); // array vs. single

单纯函数重载是搞不定这些的(参数表都相同怎么重载),而起到决议作用的恰恰上上面的 _MakeUniq。_MakeUniq 的模板重载决议发挥作用进行匹配。[b]这真是一件非常非常奇妙的事情,函数通过在返回类型上加 trait 进行重载。[/b] __invalid_type, __array,__single_object,他们指定了匹配的方向(T[N], T[], T)。

构造时候做的事情都很显而易见了。有一个小 trait, remove_extent,功能是拿掉 T[] 中的 [] 得到 T。

总结一下~

  1. 土人第一次学到 empty class optimization = =
  2. trait 真的非常有意思。enable_if, is_base_of, is_convertible 等等。模板在编译器的多态非常强大,比如刚才编译器动态决定继承关系(或者说类型)。
  3. 通过 unqiue_ptr 复习各种操作符重载,move,swap ,构造等等的写法。
> 本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 <
blog comments powered by Disqus