再议C++智能指针
背景
C++里的内存管理是个老生常谈的问题,归根结底还是因为C++内存管理的复杂性导致的。在产品研发过程中由内存导致的使用率、稳定性、性能等问题屡见不鲜,即便是诸如Google, Apple, Microsoft等世界一线大厂产品漏洞里也还是内存问题居多。
随着C++的版本演绎,目前C++标准里最好的方法还是使用智能指针,但智能指针就是那颗银弹吗?在开发和Review跨平台Windows/Linux/macOS版本客户端的时候,发现产品中大量使用不同实现的智能指针(std::auto_ptr, std::unique_ptr, std::shared_ptr, std::weak_ptr, CEF中的scoped_refptr,Chromium.base中的scoped_refptr),同时我本身之前并未用过scoped_refptr,带着如下两个疑问,对此研究了一番,遂总结此文。
- std::shared_ptr作为参数类型,值传递还是引用传递?
- CEF及Chromium都是使用C++ 11/14编译的,标准库已经支持Smart Pointers,为什么还要自己实现?
std::auto_ptr
标准库里最早的智能指针,因为一些固有特性容易导致使用问题,现在已经不推荐使用(很多时候会被禁止使用)。
项目代码里还有些地方在使用auto_ptr,个人认为不是不可以用,但要慎用。
使用auto_ptr的注意事项:
- auto_ptr 不能指向数组
- auto_ptr 不能共享所有权
- auto_ptr 不能通过复制操作来初始化
- auto_ptr 不能放入容器中使用
- auto_ptr 不能作为容器的成员
- 不能把一个原生指针给两个智能指针对象管理(对所有的智能指针)
std::shared_ptr
由于auto_ptr的在对象所有权上的局限性,C++ 11使用shared_ptr来代替auto_ptr,在使用引用计数的机制上提供了可以共享所有权的智能指针。shared_ptr比auto_ptr更安全,shared_ptr是可以拷贝和赋值的,拷贝行为也是等价的,并且可以被比较,这意味这它可被放入标准库的容器中,shared_ptr在使用上与auto_ptr类似。
std::weak_ptr
shared_ptr里引用计数的出现,解决了对象独占的问题,但又引入了新的问题:循环引用。使用weak_ptr可以打破这种循环,因为weak_ptr不会增加引用计数,使得引用形不成环,最后就可以正常的释放内部的对象,不会造成内存泄漏。
enable_shared_from_this/shared_from_this/weak_from_this
至此,还有一个问题没有解决:同一个指针被不同的shared_ptr捕获,会导致double free,因为shared_ptr根本认不知道传进来的指针变量是不是之前已经传过。
参考文献【2】示例代码
C++ 11引入enable_shared_from_this来解决这个问题,每个类都继承enable_shared_from_this,该类中有一个的成员变量(__weak_this_),如下libcxx中的实现:
template<class _Tp>
class _LIBCPP_TEMPLATE_VIS enable_shared_from_this
{
mutable weak_ptr<_Tp> __weak_this_;
protected:
_LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR
enable_shared_from_this() _NOEXCEPT {}
_LIBCPP_INLINE_VISIBILITY
enable_shared_from_this(enable_shared_from_this const&) _NOEXCEPT {}
_LIBCPP_INLINE_VISIBILITY
enable_shared_from_this& operator=(enable_shared_from_this const&) _NOEXCEPT
{return *this;}
_LIBCPP_INLINE_VISIBILITY
~enable_shared_from_this() {}
public:
_LIBCPP_INLINE_VISIBILITY
shared_ptr<_Tp> shared_from_this()
{return shared_ptr<_Tp>(__weak_this_);}
_LIBCPP_INLINE_VISIBILITY
shared_ptr<_Tp const> shared_from_this() const
{return shared_ptr<const _Tp>(__weak_this_);}
#if _LIBCPP_STD_VER > 14
_LIBCPP_INLINE_VISIBILITY
weak_ptr<_Tp> weak_from_this() _NOEXCEPT
{ return __weak_this_; }
_LIBCPP_INLINE_VISIBILITY
weak_ptr<const _Tp> weak_from_this() const _NOEXCEPT
{ return __weak_this_; }
#endif // _LIBCPP_STD_VER > 14
template <class _Up> friend class shared_ptr;
};
在enable_shared_from_this类中,没有看到给成员变量__weak_this_初始化赋值的地方,那究竟是如何保证__weak_this_拥有着类对象的指针的呢?
现在来看看shared_ptr是如何初始化的,shared_ptr定义了如下构造函数,代码同样来自libcxx:
template<class _Tp>
template<class _Yp>
shared_ptr<_Tp>::shared_ptr(const weak_ptr<_Yp>& __r,
typename enable_if<is_convertible<_Yp*, element_type*>::value, __nat>::type)
: __ptr_(__r.__ptr_),
__cntrl_(__r.__cntrl_ ? __r.__cntrl_->lock() : __r.__cntrl_)
{
if (__cntrl_ == 0)
__throw_bad_weak_ptr();
}
从以上代码可看到shared_ptr将weak_ptr指针和引用数同时捕获。
_LIBCPP_INLINE_VISIBILITY
shared_ptr<_Tp> shared_from_this()
{return shared_ptr<_Tp>(__weak_this_);}
_LIBCPP_INLINE_VISIBILITY
shared_ptr<_Tp const> shared_from_this() const
{return shared_ptr<const _Tp>(__weak_this_);}
#if _LIBCPP_STD_VER > 14
_LIBCPP_INLINE_VISIBILITY
weak_ptr<_Tp> weak_from_this() _NOEXCEPT
{ return __weak_this_; }
_LIBCPP_INLINE_VISIBILITY
weak_ptr<const _Tp> weak_from_this() const _NOEXCEPT
{ return __weak_this_; }
#endif // _LIBCPP_STD_VER > 14
在使用shared_from_this()方法时,就可以将对象引用计数在各个shared_ptr之间共享(关键就在于enable_shared_from_this里的__weak_this_中转引入计数)。
std::unique_ptr
很多时候我们可能并不想共享对象指针,所以unique_ptr是C++ 11提供的独享被管理对象指针所有权的智能指针。unique_ptr对象包装一个原始指针,并负责其生命周期。unique_ptr独享所有权,无法复制,只能移动。
scoped_refptr in CEF
产品客户端使用了Native和H5异构的模式来实现,H5容器使用了流行的Chromium Embedded Framework(CEF),这个框架有一个智能指针scoped_refptr的实现。
- 代码来自values_unittest.cc
- scoped_ptr代码在include/base/cef_ref_counted.h
- CefRefPtr是scoped_refptr的别名
// Used to test access of dictionary data on a different thread.
class DictionaryTask : public CefTask {
public:
DictionaryTask(CefRefPtr<CefDictionaryValue> value,
char* binary_data,
size_t binary_data_size)
: value_(value),
binary_data_(binary_data),
binary_data_size_(binary_data_size) {}
void Execute() override {
TestDictionary(value_, binary_data_, binary_data_size_);
}
private:
CefRefPtr<CefDictionaryValue> value_;
char* binary_data_;
size_t binary_data_size_;
IMPLEMENT_REFCOUNTING(DictionaryTask);
};
如上代码中可以看到宏:IMPLEMENT_REFCOUNTING,看下宏的定义:
- 代码在inlude/cef_base.h中
#define IMPLEMENT_REFCOUNTING(ClassName) \
public: \
void AddRef() const OVERRIDE { ref_count_.AddRef(); } \
bool Release() const OVERRIDE { \
if (ref_count_.Release()) { \
delete static_cast<const ClassName*>(this); \
return true; \
} \
return false; \
} \
bool HasOneRef() const OVERRIDE { return ref_count_.HasOneRef(); } \
bool HasAtLeastOneRef() const OVERRIDE { \
return ref_count_.HasAtLeastOneRef(); \
} \
\
private: \
CefRefCount ref_count_
一看便知,CEF中的智能指针是直接侵入用户定义的类,加个引用计数相关的成员和方法。这样做的好处是,引用计数保存在对象内,不管哪个scoped_ptr捕获到这个对象,都可执行对象的引用,释放等操作。
scoped_refptr in Chromium
Chromium Embedded Framework是基于Chromium二次开发的,暴露出来的接口也只是CEF使用到的接口。Chromium作为世界*开源项目,支持模块化编译,有许多基础模块可以单独使用。产品中就使用了Chromium.base这个库(据说Google内部也大量使用),对于跨平台开发来说是个易用,稳定,安全的三方库,推荐使用。
- base/memory/ref_counted.h
- RefCounted
- RefCountedBase
从base/memory/ref_counted_unittest.cc中找一个示例代码:
class SelfAssign : public base::RefCounted<SelfAssign> {
protected:
virtual ~SelfAssign() = default;
private:
friend class base::RefCounted<SelfAssign>;
};
用户类继承自RefCounted,而RefCounted继承自RefCountedBase,查看相关代码(太长,略做裁剪):
class RefCountedBase {
...
protected:
...
void AddRef() const {
AddRefImpl();
}
// Returns true if the object should self-delete.
bool Release() const {
ReleaseImpl();
return ref_count_ == 0;
}
private:
...
void AddRefImpl() const { ++ref_count_; }
void ReleaseImpl() const { --ref_count_; }
mutable uint32_t ref_count_ = 0;
...
DISALLOW_COPY_AND_ASSIGN(RefCountedBase);
};
由此可以看到,Chromium.base中也是将引用计数侵入到对象本身,只不是实现方式不一样,不是定义宏,而是在基类中实现。
总结
问题1:std::shared_ptr作为参数,值传递还是引用传递?
- 引用传递,线程不安全,因为传入的shared_ptr可能会在其他线程reset, 从而导致当前线程Crash
- 值传递,引用计数会+1, 线程安全。开销变大,共享指针传值的开销(引用计数和保证线程安全的开销)
- 有时可以用const std::shared_ptr来防止reset
- 无论如何,想清楚再用!!!
问题2:CEF及Chromium都是使用C++ 11/14编译的,标准库已经支持Smart Pointers,为什么还要自己实现?
- 标准库为了照顾通用性,虽实现复杂,但方便使用
- 两个版本的scoped_refptr都将引用计数侵入到类对象里,对开发人员要求较高,需要在自定义类时做更多额外工作
- 三种Smart Pointers的实现原理不一样,但效果差不多。猜测可能是因为早期版本的CEF或Chromium里用的是C++ 0x03,没有智能指针,所以CEF和Chromium实现了一套自己的方案。
C++标准库的智能指针的演进是一个发现问题,解决问题的过程。总之,这些智能指针都可以使用,前提是对它们的原理都知晓,不然很可能有隐患。开发过程中尽量统一使用一种,这样可以避免各版本差异带来的问题。