再议C++智能指针

再议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,带着如下两个疑问,对此研究了一番,遂总结此文。

  1. std::shared_ptr作为参数类型,值传递还是引用传递?
  2. 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作为参数,值传递还是引用传递?

  1. 引用传递,线程不安全,因为传入的shared_ptr可能会在其他线程reset, 从而导致当前线程Crash
  2. 值传递,引用计数会+1, 线程安全。开销变大,共享指针传值的开销(引用计数和保证线程安全的开销)
  3. 有时可以用const std::shared_ptr来防止reset
  4. 无论如何,想清楚再用!!!

问题2:CEF及Chromium都是使用C++ 11/14编译的,标准库已经支持Smart Pointers,为什么还要自己实现?

  1. 标准库为了照顾通用性,虽实现复杂,但方便使用
  2. 两个版本的scoped_refptr都将引用计数侵入到类对象里,对开发人员要求较高,需要在自定义类时做更多额外工作
  3. 三种Smart Pointers的实现原理不一样,但效果差不多。猜测可能是因为早期版本的CEF或Chromium里用的是C++ 0x03,没有智能指针,所以CEF和Chromium实现了一套自己的方案。

C++标准库的智能指针的演进是一个发现问题,解决问题的过程。总之,这些智能指针都可以使用,前提是对它们的原理都知晓,不然很可能有隐患。开发过程中尽量统一使用一种,这样可以避免各版本差异带来的问题。

参考

  1. smart pointers
  2. std::enable_shared_from_this
  3. Object ownership and calling conventions
  4. std::shared_ptr 作为参数传递的时候,到底应不应该用引用?
上一篇:eclipse下安装插件


下一篇:Android屏幕适应详解(二)