在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的,而且具有传染性,所以往往适用不了。
【未完待续】
auto_ptr
shared_ptr
正确使用shared_ptr
构造函数、删除器与分配器
std::shared_ptr
的构造函数有12种之多,这里只列举几种重要的
1 | // 构造一个空的智能指针 |
删除器和分配器构成了智能指针的主要特性之一。我们知道智能指针的重要特点就是自动帮助我们管理资源,它们解决了何时销毁对象的难题WHEN,但同时也让我们可以自行定义如何创建和销毁对象的次要问题HOW。标准库为我们提供了两个标准的std::default_delete<T>
的实现,其内容是非常的简单,即直接调用delete
和delete []
,在这里列出了后者的实现。
1 | template<class _Ty> struct default_delete<_Ty[]> |
在这里需要注意,在C++17前后,std::shared_ptr
创建数组的行为仍然是不同的。
在C++17前
创建数组的时候需要手动指定删除器1
std::shared_ptr<int> sp(new int[10], array_deleter<int>());
从C++17开始
创建数组需要手动指定数组类型int[]
,而不再可以使用int
了。1
std::shared_ptr<int[]> sp(new int[10]);
循环引用与weak_ptr
正确使用shared_ptr与裸指针
我们知道智能指针是有传染性的,这意味着我们要避免同时使用raw pointer和智能指针,也要注意不能显式或者隐式地让多个智能指针同时管理同一个raw pointer。我们进一步地探讨这个问题,shared_ptr
的主要创建方式有三种:
make_shared
函数
这个函数是*Effective Modern C++*所推荐的示例,它会创建一个控制块和一个对象。根据cppreference的介绍,这个函数有5个重载1
2
3
4
5
6
7
8
9template<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);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
,这时候我们和上面的裸指针是类似的。
使用shared_from_this传出this
下面看一个典型的错误,我们试图在类T里面,传出shared_ptr指针。错误的原因是返回的shared_ptr
采用独立的控制块,这导致this同时被两组share_ptr
管理,会导致double free的问题。
1 | class Widget { |
仔细一想,原因是这里面的this
是一个raw pointer,那在内部直接定义一个std::shared_ptr<T*>(this)
么,然后返回么?别的不论,这毫无疑问会导致循环引用。其实在对象内部传出this
是非常常见的,例如bind
系列的函数,会使用this
作为一个context。为了能够正确使用this
,就得继承一个std::enable_shared_from_this<T>
,这样就可以使用shared_from_this()
这个shared_ptr
作为this
的化身,如下所示。
1 | struct Widget: public std::enable_shared_from_this<Widget>{ |
书中甚至对这种继承一个以自己为模板参数的父类的方法介绍了一种专门的称呼,叫The Curiously Recurring Template Pattern(CRTP)。
在使用shared_from_this()
时我们需要注意以下问题:
- 二次析构
- 在构造函数中不能使用
shared_from_this()
,否则会抛出std::bad_weak_ptr
这个类的实现原理会在后面介绍。
使用make函数而不是使用智能指针的构造函数
这个来自于*Effective Modern C++*的条款21。原因之一是make函数是异常安全的,下面的代码可能导致内存泄露
1 | processWidget(std::shared_ptr<Widget>(new Widget), computePriority()); // potential resource leak! |
原因是什么呢?我们考虑这个调用的过程可以分为下面三个阶段:
- 创建
new Widget
- 构造
std::shared_ptr<Widget>
- 计算
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 | template<class _Ty, class... _Types> inline |
此外从之前的讨论中我们看到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 | // shared_ptr的基类 |
_Ref_count_base
就是所有控制块对象的基类。它主要包含两个成员_Uses
和_Weaks
,表示管理的shared_ptr
和weak_ptr
的数量。注意在涉及引用计数的部分,都要是原子的,这里默认使用了Windows的互锁函数,详情可参见文章《并发编程重要概念及比较》
1 | class _Ref_count_base |
如果强引用数为0,则销毁持有的对象,并自减弱引用数。
如果弱引用数为0,则销毁公共引用块。
1 | ... |
初始化过程的实现
进一步研究上面列出的构造函数中的实现,我们发现它们引用了下面三个函数之一,分别是适用于构造函数是否指定了Deleter和Allocator的情况。这里看到shared_ptr
的某些构造函数是会抛出异常的,为了handle住异常,书中的best practice建议创建智能指针使用make_XXX
而不是构造函数。
首先查看三个_Resetp
函数,这些函数用来接管一个裸指针_Px
。此时控制块肯定是不存在的,因此_Resetp
需要创建一个全新的控制块,因此这些函数实际上对应通过裸指针创建shared_ptr
的构造函数。
reset
函数是shared_ptr
中的一个重要的成员函数。它的作用是释放当前管理的对象,在调用后*this
就不再有效,并且被释放管理对象的控制块的引用计数会减1。reset
函数在释放之外,还可以同时接受一个新的指针作为参数,表示管理这个新的指针。我们稍后会看到一组_Reset
函数,它们则处理较为复杂的情况。
1 | private: |
_Resetp0
_Resetp0
是所有_Resetp
的终点,包含了两个调用,我们将对此进行探讨
1 | public: |
this->_Reset0
_Reset0
基类_Ptr_base
中有定义,并且派生类std::shared_ptr
也没有进行覆盖,它的功能是切换智能指针管理另一个资源。可以看到,如果此时智能指针已经绑定了控制块,那么就调用_Decref
自减一次。代码可查看上面_Ptr_base
的实现。因为稍后智能指针即将管理新的_Other_rep
控制块和_Other_ptr
对象指针了。容易看到,在被_Resetp0
调用时_Rep
是空指针,所以直接赋值。1
2
3
4
5
6
7
8void _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
18template<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);
}_Enable_shared
这里的_Enable_shared
用来处理继承了enable_shared_from_this<T>
的情况,在下面的讨论中详细了解有关这个函数和enable_shared_from_this
的实现。
enable_shared_from_this
上文讨论了当需要传出this
时,应当让类继承enable_shared_from_this
,查看一下这个类模板
1 | template<class _Ty> class enable_shared_from_this |
原来shared_from_this
就是从weak_ptr
创建一个shared_ptr
,这个weak_ptr
是创建控制块时通过_Resetp0 -> _Enable_shared -> _Do_enable
得到的,而它实际上也是指向了控制块_Refptr
。
【Q】然后发现,这里会有一个weak_ptr
,于是有以下问题:
- 这个指针是干嘛的?
回顾一下问题,要在T
里面搞出一个函数shared_from_this
,返回指向自己的shared_ptr<T>
。
解决方案很朴素,找个地方存一个shared_ptr<T>
不就行了?考虑到没法逐个修改T
本身,于是实现一个公共的enable_shared_from_this<T>
来做这个事情。 - 为什么一定得是Weak的?
因为上面说的存shared_ptr<T>
实际上是不行的。存这玩意,实际上就是在T
里面持有了一个shared_ptr<T>
,这不循环引用了么,所以得用weak_ptr<T>
。 shared_from_this
如何通过weak_ptr<T>
返回最终的shared_ptr<T>
?
通过weak_ptr牵线搭桥,就可以直接创建一个shared_ptr
了。
1 | template<class _Ty> class enable_shared_from_this |
函数_Do_enable
是个自由函数,因为它用来沟通std::shared_ptr<T>
和std::enable_shared_from_this<T>
这两个类。它接受三个参数,分别是托管对象的指针、enable_shared_from_this
指针和控制块指针。由于托管对象继承了enable_shared_from_this
,所以这1和2这两个指针其实是一样的,我们将看到在_Enable_shared
函数中直接进行了强转。
1 | ... |
继续看上面提到的_Enable_shared
函数,这里实际上是一个SFINAE,如果我们的类继承了enable_shared_from_this<T>
,那么就会执行_Do_enable
函数
1 | template<class _Ty> |
下面查看这个关键的_Do_enable
函数,实际上就是让weak_ptr指向对应的控制块。
1 | template<class _Ty1, class _Ty2> |
别名使用构造函数和owner_before
在shared_ptr
的定义中,有一个奇特的别名使用构造函数(aliasing constructor)。它管理一个指针r
,但同时指向另外一个unrelated且unmanaged指针ptr
。这个用法看似奇怪,但我们来考虑下面的两个问题:
shared_ptr
管理的对象,和指向的对象,是否一定要是同一个对象呢?- 如何创建一个指向
shared_ptr
管理对象成员的shared_ptr
?
我们的答案是:
- 不一定。我们知道
shared_ptr
之间会共享一个引用计数块,表示自己管理的对象的生存周期,但是shared_ptr
可能实际指向另一个对象。 - 可以,通过别名使用构造函数。
1 | template<class Y> |
在下面的代码中,我们构造一个Father
,它持有一个Son
的实例,现在我们创建一个智能指针son
,它持有father
,但是却指向了&father->son
。它负责管理father
的生命周期,但调用get
会返回son
的指针。
1 | struct Son { |
下面输出为2和2。
1 | printf("%d\n", father.use_count()); |
下面我们尝试通过shared_ptr::reset
方法来释放father指针对其管理的Father
对象的引用。
1 | // 这时候Father对象的引用计数为2,我们不对Son来计算引用计数 |
下面的输出为0和1,以及1和0。
1 | printf("%d\n", father.use_count()); |
【Q】看到这里有个疑问,为什么father.use_count()
就是0了,不是它给son做了alias constructor了么,怎么说也得是1啊,并且这个说明也展示了这一点。这里需要注意,在reset之后,这个shared_ptr就不指向实际的Father对象了,因此我们不能对它调用use_count
。但为了验证它依然存在,我们可以跟踪Father的析构函数。并且,我们看到son的引用计数也因为father.reset()
变成了1。
1 | // 这时候Father对象仍然存在,并且引用计数为1 |
因此可以发现,当我们需要将智能指针p
指向一个对象father
的某个字段,并且这个字段是一个依赖于该对象的智能指针的时候,我们需要使用aliasing constructor,从而保证当p
不销毁时,father
也一直存在。
此时如果使用operator<
比较shared_ptr
的大小关系就会发现它们不等,因为指向的对象不同。但此时应当owner_before
用来比较两个shared_ptr
之间的“大小关系”。
1 | std::shared_ptr<Father> father = std::make_shared<Father>(Son()); |
unique_ptr
unique_ptr
实际上相当于一个安全性增强了的auto_ptr
。
容易想到,unique_ptr
并不能被复制,所以它没有复制构造函数和复制赋值运算符。unique_ptr
的使用标志着控制权的转移,如果有熟悉Rust的朋友应该会对此感触比较深了。
其实,一般如果实现一个简单的RAII,用unique_ptr
也是可以的,毕竟它能handle住发生异常的情况。
同样,因为删除数组要用到delete []
,所以对于数组有个偏特化版本。
weak_ptr
weak_ptr
表示一个非所有性的访问。
可以通过expired
检查自己持有的对象是否已经被删除。
可以通过lock
将自己转换为一个shared_ptr
。
智能指针和容器联合使用
下面那种方式好呢?
我们知道emplace_back
是接受一个prvalue,然后把它move进去。而push_back
也可以接受一个右值,并move进去,所以1和2是没区别的。
1 | my_vector.push_back(std::make_unique<Foo>("constructor", "args")); |
Reference
- https://zhuanlan.zhihu.com/p/47744606
一个对别名构造函数的介绍。 - https://stackoverflow.com/questions/29089227/push-back-or-emplace-back-with-stdmake-unique
对emplace_back/push_back unique_ptr的分析。 - http://blog.guorongfei.com/2017/01/25/enbale-shared-from-this-implementaion/
对shared_from_this的讲解。