4

智能指针的辅助函数 std::make_shared 和 std::enable_shared_from_this

 3 years ago
source link: https://zhiqiang.org/coding/std-make-shared-enable-shared-from-this.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

智能指针的辅助函数 std::make_shared 和 std::enable_shared_from_this

作者: 张志强

, 发表于 2020-01-12

, 共 4640 字 , 共阅读 846 次

前面已经提到std::shared_ptr有三个缺陷:

  1. shared_ptr对象共用引用计数器,计数器本身需通过new放在堆上。而new会引起性能问题。
  2. 引用计数的内存区域和数据区域不一致,缓存失效导致性能问题。
  3. 编写代码不善,将导致同一个数据,绑定到了两个引用计数,从而导致双重删除。

boost::intrusive_ptr可以解决这些问题。但std::shared_ptr也提供了配套函数来试图解决。这就是std::make_sharedstd::enable_shared_from_this

1. std::make_shared

std::make_shared的作用是在为数据分配空间的同时,分配计数器的空间(从实际实现是反过来,在分配计数器空间的空时,多分配一块空间给数据用),让数据和计数器在连续的内存区域,对 CPU 缓存也友好。同时不用暴露原始指针,也降低了第三个问题的出现几率:

auto xp = std::make_shared<int>(1);

它的实现依赖于shared_ptr的构造函数:

template<typename _Tp, typename... _Args>
inline shared_ptr<_Tp> make_shared(_Args&&... __args) {
    typedef typename std::remove_const<_Tp>::type _Tp_nc;
    return __shared_ptr<_Tp>(_Sp_make_shared_tag(), std::allocator<_Tp_nc>(),
                            std::forward<_Args>(__args)...);
}

这里_SP_make_shared_tag是一个空的helper类,只是为了区别构造函数的不同重载。实际调用的构造函数如下:

template<typename _Alloc, typename... _Args>
__shared_ptr(_Sp_make_shared_tag __tag, const _Alloc& __a, _Args&&... __args)
    : _M_ptr(), _M_refcount(__tag, (_Tp*)0, __a, std::forward<_Args>(__args)...)
{
    void* __p = _M_refcount._M_get_deleter(typeid(__tag));
    _M_ptr = static_cast<_Tp*>(__p);
    _M_enable_shared_from_this_with(_M_ptr);
}

实际内存分配发生在_M_refcount的构造函数:

template<typename _Tp, typename _Alloc, typename... _Args>
__shared_count(_Sp_make_shared_tag, _Tp*, const _Alloc& __a, _Args&&... __args) : _M_pi(0) {
    typedef _Sp_counted_ptr_inplace<_Tp, _Alloc, _Lp> _Sp_cp_type;
    typename _Sp_cp_type::__allocator_type __a2(__a);
    auto __guard = std::__allocate_guarded(__a2);
    _Sp_cp_type* __mem = __guard.get();
    ::new (__mem) _Sp_cp_type(std::move(__a), std::forward<_Args>(__args)...);
    _M_pi = __mem;
    __guard = nullptr;
}

构造原始数据则藏在_Sp_counted_ptr_inplace构造函数中。这个类是计数器的基类,里面不光包含了计数器,也包含了数据对象:

template<typename _Tp, typename _Alloc, _Lock_policy _Lp>
class _Sp_counted_ptr_inplace final : public _Sp_counted_base<_Lp> {
    // actual data object
    struct _Impl : _Sp_ebo_helper<0, _Alloc> {
        __gnu_cxx::__aligned_buffer<_Tp> _M_storage;
    };
    _Impl _M_impl;

public:
    template<typename... _Args>
    _Sp_counted_ptr_inplace(_Alloc __a, _Args&&... __args) : _M_impl(__a) {
        allocator_traits<_Alloc>::construct(__a, _M_ptr(), std::forward<_Args>(__args)...); 
    }
}

所以make_shared将导致数据的内存空间合并到了计数器。而我们知道数据的存续期将早于计数器,这使得即使数据被销毁,如果计数器还没销毁,数据的内存空间将不会被释放!这和boost::intrusive的设计完全相反。

2. std::enable_shared_from_this

std::enable_shared_from_this是一种侵入式设计,和boost::intrusive差不多:

class T : public std::enable_shared_from_this {
public:
     int age;
     std::string name;
};

T* t = new T();
// 下面的shared1, shared2和weak都使用同一个计数器。
std::shared_ptr<T> shared1 = t;
// 只有t已经被一个shared_ptr托管时,shared_from_this()才有效,否则将抛出异常。
std::shared_ptr<T> shared2 = t->shared_from_this();
std::weak_ptr<T> weak = t->weak_from_this();

其中std::enable_shared_from_this的实现很简单:

template<typename _Tp>
class enable_shared_from_this
{
public:
    shared_ptr<_Tp> shared_from_this()
    { return shared_ptr<_Tp>(this->_M_weak_this); }

    shared_ptr<const _Tp> shared_from_this() const
    { return shared_ptr<const _Tp>(this->_M_weak_this); }

    weak_ptr<_Tp> weak_from_this() noexcept
    { return this->_M_weak_this; }

    weak_ptr<const _Tp> weak_from_this() const noexcept
    { return this->_M_weak_this; }

protected:
    template<typename _Tp1> 
    void _M_weak_assign(_Tp1* __p, const __shared_count<>& __n) const noexcept
    { _M_weak_this._M_assign(__p, __n); }

private:
    mutable weak_ptr<_Tp>  _M_weak_this;
};

注意这里的_M_weak_this默认没有做初始化,因此数据没有被托管时,shared_from_this()无效。但一旦做了初始化,比如执行了std::shared_ptr<int> shared1 = t时,将调用下面初始化函数:

template<typename _Yp, typename = _SafeConv<_Yp>>
explicit __shared_ptr(_Yp* __p) : _M_ptr(__p), _M_refcount(__p, typename is_array<_Tp>::type())
{
    _M_enable_shared_from_this_with(__p);
}

_M_enable_shared_from_this_with则负责将计数器写入数据对象中:

// if _Yp base on std::enable_shared_from_this.
template<typename _Yp, typename _Yp2 = typename remove_cv<_Yp>::type>
typename enable_if<__has_esft_base<_Yp2>::value>::type _M_enable_shared_from_this_with(_Yp* __p) noexcept {
    if (auto __base = __enable_shared_from_this_base(_M_refcount, __p))
        __base->_M_weak_assign(const_cast<_Yp2*>(__p), _M_refcount);
    }
}

从实现上看,std::enable_shared_from_this效率远没有boost::intrusive_ptr高。因为侵入数据的只是计数器的指针,并不是计数器本身。

Q. E. D.

avatar-0.jpg

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK