再议C++智能指针

简介: # 再议C++智能指针 ## 背景 C++里的内存管理是个老生常谈的问题,归根结底还是因为C++内存管理的复杂性导致的。在产品研发过程中由内存导致的使用率、稳定性、性能等问题屡见不鲜,即便是诸如Google, Apple, Microsoft等世界一线大厂产品漏洞里也还是内存问题居多。 随着C++的版本演绎,目前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
相关文章
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
74 4
|
2月前
|
存储 安全 编译器
在 C++中,引用和指针的区别
在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
|
2月前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
56 1
|
2月前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
40 2
|
2月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
2月前
|
存储 C++ 索引
C++函数指针详解
【10月更文挑战第3天】本文介绍了C++中的函数指针概念、定义与应用。函数指针是一种指向函数的特殊指针,其类型取决于函数的返回值与参数类型。定义函数指针需指定返回类型和参数列表,如 `int (*funcPtr)(int, int);`。通过赋值函数名给指针,即可调用该函数,支持两种调用格式:`(*funcPtr)(参数)` 和 `funcPtr(参数)`。函数指针还可作为参数传递给其他函数,增强程序灵活性。此外,也可创建函数指针数组,存储多个函数指针。
|
3月前
|
编译器 C++
【C++核心】指针和引用案例详解
这篇文章详细讲解了C++中指针和引用的概念、使用场景和操作技巧,包括指针的定义、指针与数组、指针与函数的关系,以及引用的基本使用、注意事项和作为函数参数和返回值的用法。
52 3
|
2月前
|
存储 编译器 程序员
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(二)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
3月前
|
C++
C++(十八)Smart Pointer 智能指针简介
智能指针是C++中用于管理动态分配内存的一种机制,通过自动释放不再使用的内存来防止内存泄漏。`auto_ptr`是早期的一种实现,但已被`shared_ptr`和`weak_ptr`取代。这些智能指针基于RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。RAII确保对象在其生命周期结束时自动释放资源。通过重载`*`和`-&gt;`运算符,可以方便地访问和操作智能指针所指向的对象。
|
3月前
|
C++
C++(九)this指针
`this`指针是系统在创建对象时默认生成的,用于指向当前对象,便于使用。其特性包括:指向当前对象,适用于所有成员函数但不适用于初始化列表;作为隐含参数传递,不影响对象大小;类型为`ClassName* const`,指向不可变。`this`的作用在于避免参数与成员变量重名,并支持多重串联调用。例如,在`Stu`类中,通过`this-&gt;name`和`this-&gt;age`明确区分局部变量与成员变量,同时支持链式调用如`s.growUp().growUp()`。