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

作者: , 共 4640 字 , 共阅读 0

前面已经提到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.

类似文章:
智能指针在现代 C++里用得越多。以前只知道它大致的原理,比如使用引用计数。但很多实现细节并不清楚,最直接的,它是如何实现多线程安全的?今天找了 gnu c++ library 的实现好好看了一下。
编程 » C++, 智能指针
理论上而言,当 C++提供了std::unique_ptr, C++的程序就不应该出现普通指针了。所有普通指针都可以用std::unique_ptr代替,避免手动删除对象。
编程 » C++, 异步
C++11 的标准异步库至少包含下面内容:
编程 » C++, C++标准库
std::tuple的原理并不复杂,但有些细节非常有意思。其中有一个是至少在gnu C++ std的实现中,std::tuple是倒序存储的:
编程 » folly, C++, 数据容器
由 Facebook 开发和维护的 C++库 Folly 提供folly::small_vector,代码文件地址:https://github.com/facebook/folly/blob/master/folly/small_vector.h
由 Facebook 开发和维护的 C++库 Folly 提供了自旋锁的实现folly::MicroSpinLock,代码文件地址:https://github.com/facebook/folly/blob/master/folly/synchronization/MicroSpinLock.h
编程 » C++, folly
高效程序总是尽量避免频繁触碰在堆上分配和释放内存,所以无论是std::string还是folly:fbstring都做了SSO( small string optimization )。而folly::FixedString是一个很有意思的实现,它可以把任意长度的字符串都放在堆上。代码可见https://github.com/facebook/folly/blob/master/folly/FixedString.h
由 Facebook 开发和维护的 C++库 Folly 提供了锁folly::MicroLock,代码文件地址:https://github.com/facebook/folly/blob/master/folly/MicroLock.h
编程 » C++, Boost, 智能指针
如果理解了侵入式容器,侵入式智能指针也很容易理解。传统的智能指针std::shared_ptr使用了和数据无关的引用计数,这带来两个问题:
编程 » C++, 异步
C++11 的标准异步库至少包含下面内容: