C++智能指针的使用与实现

在C++史前时代只有一种智能指针std::auto_ptr<T>,它的行为类似一个lock_guard<T>,或者经过封装的RAII,但在使用中发现,依托于RAII是不够的,为了方便地实现更复杂逻辑下的资源管理,我们需要从资源的所有权上对智能指针进行更加细致的分类。在C++11之后,标准库引入了std::shared_ptr<T>std::unique_ptr<T>std::weak_ptr<T>来替换之前的std::auto_ptr<T>
截至目前为止,我基本没怎么用过智能指针,一方面之前做的项目都比较局限,使用RAII或者对象池会更方便,另一方面智能指针和对C风格的兼容性也不是很好,例如很多C风格的代码要求bit-wise而不是member-wise的操作,而智能指针并不是trivial的,而且具有传染性,所以往往适用不了。
【未完待续】

shared_ptr

正确使用shared_ptr

构造函数、删除器与分配器

std::shared_ptr构造函数有12种之多,这里只列举几种重要的

1
2
3
4
5
6
7
8
9
// 构造一个空的智能指针
constexpr shared_ptr() noexcept;
constexpr shared_ptr( std::nullptr_t ) noexcept;
// 这是最常用的
template<class Y> explicit shared_ptr( Y* ptr );
// 这里在后面加了一个删除器的参数
template<class Y, class Delete > shared_ptr( Y* ptr, Deleter d );
// 这里又添加了一个分配器的函数
template<class Y, class Deleter, class Alloc> shared_ptr( Y* ptr, Deleter d, Alloc alloc );

删除器和分配器构成了智能指针的主要特性之一。我们知道智能指针的重要特点就是自动帮助我们管理资源,它们解决了何时销毁对象的难题WHEN,但同时也让我们可以自行定义如何创建和销毁对象的次要问题HOW。标准库为我们提供了两个标准的std::default_delete<T>的实现,其内容是非常的简单,即直接调用deletedelete [],在这里列出了后者的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<class _Ty> struct default_delete<_Ty[]>
{
// 一个默认的构造函数
constexpr default_delete() _NOEXCEPT = default;
// 一个默认的复制构造函数
template<class _Uty, class = typename enable_if<is_convertible<_Uty(*)[], _Ty(*)[]>::value, void>::type>
default_delete(const default_delete<_Uty[]>&) _NOEXCEPT {}

template<class _Uty, class = typename enable_if<is_convertible<_Uty(*)[], _Ty(*)[]>::value, void>::type>
void operator()(_Uty *_Ptr) const _NOEXCEPT
{
static_assert(0 < sizeof (_Uty), "can't delete an incomplete type");
delete[] _Ptr;
}
};

循环引用与weak_ptr

正确使用shared_ptr与裸指针

我们知道智能指针是有传染性的,这意味着我们要避免同时使用raw pointer和智能指针,也要注意不能显式或者隐式地让多个智能指针同时管理同一个raw pointer。我们进一步地探讨这个问题,shared_ptr的主要创建方式有三种:

  1. make_shared函数
    这个函数是Effective Modern C++所推荐的示例,它会创建一个控制块和一个对象。根据cppreference的介绍,这个函数有5个重载

    1
    2
    3
    4
    5
    6
    7
    8
    9
    template<class T, class... Args> shared_ptr<T> make_shared( Args&&... args );
    // 从C++20开始,这里T是数组U[]
    template<class T> shared_ptr<T> make_shared(std::size_t N);
    // 从C++20开始,这里T是数组U[N]
    template<class T> shared_ptr<T> make_shared();
    // 从C++20开始,这里T是数组U[]
    template<class T> shared_ptr<T> make_shared(std::size_t N, const std::remove_extent_t<T>& u);
    // 从C++20开始,这里T是数组U[N]
    template<class T> shared_ptr<T> make_shared(const std::remove_extent_t<T>& u);

    我们注意一下这里的初始化是小括号初始化而不是C++11新规定的uniform初始化,即花括号初始化,例如下面的语句会创建10个20,如果我们想放两个元素10和20进去就要显式创建一个初始化列表

    1
    2
    3
    4
    5
    6
    7
    // 10个20
    auto upv = std::make_shared<std::vector<int>>(10, 20);
    // 10, 20
    // create std::initializer_list
    auto initList = { 10, 20 };
    // create std::vector using std::initializer_list ctor
    auto spv = std::make_shared<std::vector<int>>(initList);
  2. shared_ptr构造函数
    这种情况下我们将一个裸指针传给shared_ptr,这时就可能将裸指针泄露出去,从而导致可能的double free问题。因此在Effective Modern C++的条款19中强调best practice是我们写成将new语句写到参数列表里面

    1
    std::shared_ptr<Widget> spw1(new Widget);

    特别地,我们也可以从一个unique_ptr构造shard_ptr,这时候我们和上面的裸指针是类似的。

我们不能忽视this也是一个raw pointer,但我们又不能在内部直接定义一个std::shared_ptr<T*>(this),这毫无疑问会导致循环引用。更严重的是在对象内部传出this是非常常见的,例如bind系列的函数,会使用this作为一个context。为了能够正确使用this,就得让我们的类继承一个enable_shared_from_this<T>,并传入一个shared_from_this()作为this的化身。写起来的代码是类似这样的,书中甚至对这种继承一个以自己为模板参数的父类的方法介绍了一种专门的称呼,叫The Curiously Recurring Template Pattern(CRTP)。

1
2
3
4
5
struct Widget: public std::enable_shared_from_this<Widget>{
void process(){
processedWidgets.emplace_back(shared_from_this());
}
};

我们下面来探究一下这个类的实现原理。它首先寻找当前对象的控制块,然后创建一个新的std::shared_ptr来引用那个控制块。

使用make函数而不是使用智能指针的构造函数

这个来自于Effective Modern C++的条款21。原因之一是make函数是异常安全的,下面的代码可能导致内存泄露

1
processWidget(std::shared_ptr<Widget>(new Widget), computePriority()); // potential resource leak!

原因是什么呢?我们考虑这个调用的过程可以分为下面三个阶段:

  1. 创建new Widget
  2. 构造std::shared_ptr<Widget>
  3. 计算computePriority()

根据C++标准,这三个的求值顺序是UB的。我们考虑编译器产生1/3/2这样的执行顺序,并且此时computePriority()产生了异常,此时步骤1中new出来的对象就泄露了。
此外,对shared_ptr来说,使用make_shared函数还能提高效率。这是由于创建new Widget和控制块分配两次内存,而使用make_shared函数可以一次分配完。我们来看看标准库的实现,在这里我们看到只分配了一个_Ref_count_obj<_Ty>的对象,这个对象实际上继承了我们上面看到的_Ref_count_base的子类,它有一个typename aligned_union<1, _Ty>::type _Storage的字段管理了我们实际的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<class _Ty, class... _Types> inline
shared_ptr<_Ty> make_shared(_Types&&... _Args)
{ // make a shared_ptr
_Ref_count_obj<_Ty> *_Rx =
new _Ref_count_obj<_Ty>(_STD forward<_Types>(_Args)...);

shared_ptr<_Ty> _Ret;
_Ret._Resetp0(_Rx->_Getptr(), _Rx);
return (_Ret);
}

// In _Ref_count_obj's definition
template<class... _Types> _Ref_count_obj(_Types&&... _Args) : _Ref_count_base()
{ // construct from argument list
::new ((void *)&_Storage) _Ty(_STD forward<_Types>(_Args)...);
}

此外从之前的讨论中我们看到make_shared杜绝了我们看到裸指针的一切可能性,因为它在函数内部创建了智能指针所指向类的实例,因此也更安全。

shared_ptr的结构与实现

我们以PJ Plauger的STL实现为例来查看这个智能指针的实现

基类_Ptr_base

std::shared_ptr继承了_Ptr_base,里面持有了两个指针,第一个就是实际的裸指针_Ptr,另一个是控制块指针_Rep。所有的控制块包括_Ref_count<T>_Ref_count_del<T>_Ref_count_del_alloc<T>_Ref_count_obj<T>_Ref_count_obj_alloc<T>,都继承自_Ref_count_base

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// shared_ptr的基类
template<class _Ty>
class _Ptr_base
{ // base class for shared_ptr and weak_ptr
/* ... */
private:
// 这是真实资源的指针
_Ty *_Ptr;
// 这是shared_ptr外挂式的控制块
_Ref_count_base *_Rep;
// 所有的实例化的_Ptr_base都互为友元
template<class _Ty0>
friend class _Ptr_base;
};
};

// 所有控制块对象的基类
class _Ref_count_base
{ // common code for reference counting
private:
// 删除裸指针
virtual void _Destroy() _NOEXCEPT = 0;
// 删除自己
virtual void _Delete_this() _NOEXCEPT = 0;

private:
_Atomic_counter_t _Uses;
_Atomic_counter_t _Weaks;

protected:
// 注意在涉及引用计数的部分,都要是原子的,这里默认使用了Windows的互锁函数,详情可参见我的[博文《并发编程重要概念及比较》](/2017/12/28/Concurrency-Programming-Compare/)
//
_Ref_count_base()
{ // construct
// 在构造时我们初始化引用计数都为**1**
_Init_atomic_counter(_Uses, 1);
_Init_atomic_counter(_Weaks, 1);
}
public:
// 虚析构函数
virtual ~_Ref_count_base() _NOEXCEPT
{ // ensure that derived classes can be destroyed properly
}

bool _Incref_nz()
{ // increment use count if not zero, return true if successful
...
};

// 这里直接进行强转,以便调用互锁函数
#define _MT_INCR(x) _InterlockedIncrement(reinterpret_cast<volatile long *>(&x))
#define _MT_DECR(x) _InterlockedDecrement(reinterpret_cast<volatile long *>(&x))

void _Incref()
{ // increment use count
_MT_INCR(_Uses);
}

void _Incwref()
{ // increment weak reference count
_MT_INCR(_Weaks);
}

void _Decref()
{ // decrement use count
if (_MT_DECR(_Uses) == 0)
{ // destroy managed resource, decrement weak reference count
_Destroy();
_Decwref();
}
}

void _Decwref()
{ // decrement weak reference count
if (_MT_DECR(_Weaks) == 0)
_Delete_this();
}

...

初始化过程的实现

进一步研究上面列出的构造函数中的实现,我们发现它们引用了下面三个函数之一,分别是适用于构造函数是否指定了Deleter和Allocator的情况。这里我们看到shared_ptr的某些构造函数是会抛出异常的,所以我们会看到书中的best practice建议使用make系函数而不是构造函数进行创建。下面我们首先查看三个_Resetp函数,这些函数用来接管一个裸指针_Px,根据上面的讨论,我们知道这时候控制块肯定是不存在的,因此创建一个全新的控制块,它们实际上对应通过裸指针创建shared_ptr的构造函数。我们稍后会看到一组_Reset函数,它们则处理较为复杂的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private:
template<class _Ux>
void _Resetp(_Ux *_Px)
{ // release, take ownership of _Px
_TRY_BEGIN // allocate control block and reset
// 注意_Ref_count在创建时两个引用计数都为1了,因为它继承了_Ref_count_base,详见_Ref_count_base相关代码
_Resetp0(_Px, new _Ref_count<_Ux>(_Px));
_CATCH_ALL // allocation failed, delete resource
delete _Px;
_RERAISE;
_CATCH_END
}

template<class _Ux, class _Dx>
void _Resetp(_Ux *_Px, _Dx _Dt)
{ // release, take ownership of _Px, deleter _Dt
_TRY_BEGIN // allocate control block and reset
_Resetp0(_Px, new _Ref_count_del<_Ux, _Dx>(_Px, _Dt));
_CATCH_ALL // allocation failed, delete resource
_Dt(_Px);
_RERAISE;
_CATCH_END
}

template<class _Ux, class _Dx, class _Alloc>
void _Resetp(_Ux *_Px, _Dx _Dt, _Alloc _Ax)
{ // release, take ownership of _Px, deleter _Dt, allocator _Ax
typedef _Ref_count_del_alloc<_Ux, _Dx, _Alloc> _Refd;
typedef _Wrap_alloc<_Alloc> _Alref0;
typename _Alref0::template rebind<_Refd>::other _Alref(_Ax);

_TRY_BEGIN // allocate control block and reset
_Refd *_Pref = _Alref.allocate(1);
_Alref.construct(_Pref, _Px, _Dt, _Ax);
_Resetp0(_Px, _Pref);
_CATCH_ALL // allocation failed, delete resource
_Dt(_Px);
_RERAISE;
_CATCH_END
}

_Resetp0是所有_Resetp的终点,包含了两个调用,我们将对此进行探讨

1
2
3
4
5
6
7
public:
template<class _Ux>
void _Resetp0(_Ux *_Px, _Ref_count_base *_Rx)
{ // release resource and take ownership of _Px
this->_Reset0(_Px, _Rx);
_Enable_shared(_Px, _Rx);
}

  1. this->_Reset0
    _Reset0基类_Ptr_base中有定义,并且派生类std::shared_ptr也没有进行覆盖,它的功能是切换智能指针管理另一个资源。可以看到,如果此时智能指针已经绑定了控制块,那么就调用_Decref自减一次(代码可查看上面_Ptr_base的实现),因为稍后我们的智能指针即将管理新的_Other_rep控制块和_Other_ptr对象指针了。容易看到,在被_Resetp0调用时_Rep是空指针,所以我们直接赋值。

    1
    2
    3
    4
    5
    6
    7
    8
    void _Reset0(_Ty *_Other_ptr, _Ref_count_base *_Other_rep)
    { // release resource and take new resource
    // 这里的_Rep是_Ptr_base持有的_Ref_count_base *
    if (_Rep != 0)
    _Rep->_Decref();
    _Rep = _Other_rep;
    _Ptr = _Other_ptr;
    }

    既然如此,为什么我们不增加下_Other_rep的调用数目呢?其实是会增加的,只是不在_Other_rep之中。首先根据上面的讨论,当_Other_rep是新被创建的对象时,它的两个引用计数就默认被设为0了。其次,当_Other_rep是由其它智能指针创建的,也就是说我们此时将智能指针是从另一个智能指针创建的时,我们会调用之前提到的_Reset函数,而这个函数在自增对方的控制块_Other_rep后才会调用_Reset0

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    template<class _Ty2, class = typename enable_if<is_convertible<_Ty2 *, _Ty *>::value, void>::type>
    shared_ptr(const shared_ptr<_Ty2>& _Other) _NOEXCEPT
    { // construct shared_ptr object that owns same resource as _Other
    this->_Reset(_Other);
    }

    template<class _Ty2>
    void _Reset(const _Ptr_base<_Ty2>& _Other)
    { // release resource and take ownership of _Other._Ptr
    _Reset(_Other._Ptr, _Other._Rep);
    }

    void _Reset(_Ty *_Other_ptr, _Ref_count_base *_Other_rep)
    { // release resource and take _Other_ptr through _Other_rep
    if (_Other_rep)
    _Other_rep->_Incref();
    _Reset0(_Other_ptr, _Other_rep);
    }
  2. _Enable_shared
    这里的_Enable_shared用来处理继承了enable_shared_from_this<T>的情况,我们将在下面的讨论中详细了解有关这个函数和enable_shared_from_this的实现。

enable_shared_from_this

上文讨论了当需要传出this时,我们应当让我们的类继承template<class _Ty> class enable_shared_from_this,现在来查看一下这个类模板的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
template<class _Ty> class enable_shared_from_this
{ // provide member functions that create shared_ptr to this
public:
// 稍后我们将看到,这个_EStype被用来做SFINAE
typedef _Ty _EStype;

shared_ptr<_Ty> shared_from_this()
{
return (shared_ptr<_Ty>(_Wptr));
}

shared_ptr<const _Ty> shared_from_this() const
{
return (shared_ptr<const _Ty>(_Wptr));
}

protected:
constexpr enable_shared_from_this() _NOEXCEPT {}
enable_shared_from_this(const enable_shared_from_this&) _NOEXCEPT {}
enable_shared_from_this & operator=(const enable_shared_from_this&) _NOEXCEPT { return (*this); }
~enable_shared_from_this() _NOEXCEPT {}

private:
// 这个函数是个自由函数,它接受三个参数,分别是托管对象的指针、enable_shared_from_this指针和控制块指针。
// 由于托管对象继承了enable_shared_from_this,所以这两个指针其实是一样的,我们将看到在_Enable_shared函数中直接进行了强转
template<class _Ty1, class _Ty2> friend void _Do_enable(_Ty1 *, enable_shared_from_this<_Ty2>*, _Ref_count_base *);
weak_ptr<_Ty> _Wptr;
};

下面我们可以继续查看上面提到的_Enable_shared函数,可以看到这里实际上是一个SFINAE,如果我们的类继承了enable_shared_from_this<T>,那么就会执行_Do_enable函数

1
2
3
4
5
6
7
8
9
10
11
template<class _Ty>
inline void _Enable_shared(_Ty *_Ptr, _Ref_count_base *_Refptr, typename _Ty::_EStype * = 0)
{ // reset internal weak pointer
if (_Ptr)
_Do_enable(_Ptr, (enable_shared_from_this<typename _Ty::_EStype>*)_Ptr, _Refptr);

}

inline void _Enable_shared(const volatile void *, const volatile void *)
{ // not derived from enable_shared_from_this; do nothing
}

下面我们来查看这个关键的_Do_enable函数

1
2
3
4
5
template<class _Ty1, class _Ty2>
inline void _Do_enable(_Ty1 *_Ptr, enable_shared_from_this<_Ty2> *_Es, _Ref_count_base *_Refptr)
{
_Es->_Wptr._Resetw(_Ptr, _Refptr);
}

别名使用构造函数和owner_before

shared_ptr的定义中,我们能发现一个奇特的构造函数,即别名使用构造函数(aliasing constructor)。此时它管理一个指针,但同时指向另外一个指针。这个用法看似奇怪,但当我们考虑到如果我们将一个智能指针指向另一个智能指针所拥有的对象的某个成员时,事情变得显然了起来。

1
2
template<class Y> 
shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept;

以下面的代码为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Son { 
// some data that we want to point to
};

struct Father {
Son son;
};

shared_ptr<Father> father = make_shared<Father>(...);
shared_ptr<Son> son(father, &father->son);

// 这时候Father对象的引用计数为2,我们不对Son来计算引用计数
father.reset();

// 这时候Father对象仍然存在,并且引用计数为1
// 如果我们对Son对象计算引用计数的话,这个对象就会被销毁了
func(son);

此时如果使用operator<比较shared_ptr的大小关系就会发现它们不等,因为指向的对象不同。但此时应当owner_before用来比较两个shared_ptr之间的“大小关系”。

1
2
3
4
5
6
7
std::shared_ptr<Father> father = std::make_shared<Father>(Son());
std::shared_ptr<Son> son(father, &father->son);
printf("%d %d\n", father.owner_before(son), son.owner_before(father)); // 0 0

std::shared_ptr<Father> father2 = std::make_shared<Father>(Son());
std::shared_ptr<Son> son2 = std::make_shared<Son>();
printf("%d %d\n", father2.owner_before(son2), son2.owner_before(father2)); // 1 0

unique_ptr

unique_ptr实际上相当于一个安全性增强了的auto_ptrunique_ptr的使用标志着控制权的转移,如果有熟悉Rust的朋友应该会对此感触比较深了。