前面已经提到std::shared_ptr
有三个缺陷:
shared_ptr
对象共用引用计数器,计数器本身需通过new
放在堆上。而new
会引起性能问题。- 引用计数的内存区域和数据区域不一致,缓存失效导致性能问题。
- 编写代码不善,将导致同一个数据,绑定到了两个引用计数,从而导致双重删除。
boost::intrusive_ptr
可以解决这些问题。但std::shared_ptr
也提供了配套函数来试图解决。这就是std::make_shared
和std::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.