std::shared_ptr 学习笔记
Overview
概述
std::shared_ptr是智能指针的一种,在modern c++中被广泛使用(甚至滥用)
虽然天天使用,但是有些细节还不是100%清楚,因此来整理一下 为了方便表述,下文只写shared_ptr,不在写std的namespace.
组成
shared_ptr的实现中,成员通常由两部分组成。一个是所涵盖对象的指针,一个是control block 的指针
control block
最重要的是,control block是 dynamically-allocated 的
(校招的时候某次面试,让我手写shared_ptr的实现,当时被多个object如何共享引用计数卡住了。。主要就是没意识到control block是单独allocate的,shared_ptr的实现中只是保留一个指针)
control block中通常包含五部分
- either a pointer to the managed object or the managed object itself;
- the deleter (type-erased);
- the allocator (type-erased);
- the number of shared_ptrs that own the managed object;
- the number of weak_ptrs that refer to the managed object.
这里面有几点值得强调:
- 两个引用计数都是atomic的。
- weak_ptr是为了解决循环引用
- type-erased是什么? 后面会介绍
线程安全
shared_ptr的线程安全性快成c++面试的top10经典八股文了
简单说,shared_ptr<T>的引用计数的实现是线程安全的(通常是两个atomic变量),但是对于T的操作不是线程安全的*
type erasure(erase的名词形式)
也称为type-earsed
是指在程序运行的时候,不需要知道具体的类型。与之相反的是 type-passing semantics
实现 type-erasure semantics的主要目的是,使得程序运行时不依赖具体的类型信息。
举个例子, std::shared_ptr的constrol block中有对应的deleter. 这个deleter不需要类型也可以work是因为这个deleter做到到了"type-erasure" 也就是 确保程序在运行时执行不依赖类型信息。
从代码来看,如下代码时可以正常编译的
1#include <memory>
2class Toy; // only forward declaration
3
4std::shared_ptr<Toy> fwd (std::shared_ptr<Toy> p)
5{
6 if (!p) throw int{};
7 return p;
8}
9
why? Toy是一个in-complete type, shared_ptr为什么能成功析构?
这个是因为。。deleter的类型在创建后就被erased了。 后续析构并不需要知道deleter的具体类型
So how come shared_ptr works? It may also need to delete its pointee. Well, you probably know the answer already: shared_ptr’s deleter is type-erased. Its type is something like std::function<void(Toy*)>. shared_ptr just needs to call it, and it does not care what the deleter does. Of course, upon creation of the shared_ptr, you have to tell it exactly how the deleter should delete the object, but once the construction is done, the type of the deleter is erased
这也是为什么std::shared_ptr<void> 合法并且可以正确析构的原因
一个实例的实现是
1
2namespace detail {
3 struct deleter_base {
4 virtual ~deleter_base() {}
5 virtual void operator()( void* ) = 0;
6 };
7 template <typename T>
8 struct deleter : deleter_base {
9 virtual void operator()( void* p ) {
10 delete static_cast<T*>(p);
11 }
12 };
13}
14template <typename T>
15class simple_ptr {
16 T* ptr;
17 detail::deleter_base* deleter;
18public:
19 template <typename U>
20 simple_ptr( U* p ) {
21 ptr = p;
22 deleter = new detail::deleter<U>();
23 }
24 ~simple_ptr() {
25 (*deleter)( ptr );
26 delete deleter;
27 }
28};
29
30
这里值得注意的有两点.
-
shared_ptr的class template parameter只有一个T,但是构造函数有另外个member tempalte parameter U. 也就是说shared_ptr的deleter不属于其shared_ptr类型的一部分。 这样做增加了灵活性(同样的T可以使用不同的deleter),但是也增加了额外的开销(需要存储deleter,并且如果没有使用std::make_shard的话需要额外的空间分配). 但是shared_ptr由于存在额外的引用计数,本来就要有额外的开销
-
与之不同的是,std::unique_ptr 的class template parameter 有两个,Object类型T和deleter U. 这使得deleter是unique_ptr类型的一部分,但是带来的好处就是不需要存储deleter到unique_ptr中。
一个unique_ptr的实例:
1
2template <class T, class D = default_delete<T>>
3class unique_ptr
4{
5 unique_ptr(T*, D&); //simplified
6 ...
7};
8
如下的代码无法编译成功,会报错"default deleter cannot delete an incomplete type",
原因就是deleter是unique_ptr type的一部分
1#include <memory>
2class Toy; // only forward declaration
3
4std::unique_ptr<Toy> fwd (std::unique_ptr<Toy> p)
5{
6 if (!p) throw int{};
7 return p;
8}
9
10
一种简单的实现
这个实现中没有control block,只有一个简单的引用计数 其中用到了std::exchange 来简化移动语义的实现
最重要的一点仍然是
ControlBlock是要动态分配出来的一个指针,不然无法在不同object之间共享引用计数
1
2#include <atomic>
3#include <cstdio>
4#include <ios>
5#include <iostream>
6#include <utility>
7
8struct ControlBlock {
9 std::atomic<int> ref_count;
10 ControlBlock(int cnt) { ref_count.store(cnt); }
11};
12
13template <class T> class MySharedPtr {
14private:
15 T *ptr = nullptr;
16 ControlBlock *block = nullptr;
17
18public:
19 MySharedPtr()
20 : ptr(nullptr), block(new ControlBlock(0)) // default constructor
21 {}
22
23 MySharedPtr(T *ptr)
24 : ptr(ptr), block(new ControlBlock(1)) // constructor
25 {}
26
27 /*** Copy Semantics ***/
28 MySharedPtr(const MySharedPtr &obj) // copy constructor
29 {
30 this->ptr = obj.ptr; // share the underlying pointer
31 this->block = obj.block;
32 if (nullptr != obj.ptr) {
33 (*this->block)
34 .ref_count++; // if the pointer is not null, increment the refCount
35 }
36 }
37
38 MySharedPtr &operator=(const MySharedPtr &obj) // copy assignment
39 {
40 __cleanup__(); // cleanup any existing data
41
42 // Assign incoming object's data to this object
43 this->ptr = obj.ptr; // share the underlying pointer
44 this->refCount = obj.refCount;
45 if (nullptr != obj.ptr) {
46 (*this->refCount)++; // if the pointer is not null, increment the refCount
47 }
48 return *this;
49 }
50
51 /*** Move Semantics ***/
52 MySharedPtr(MySharedPtr &&dyingObj) // move constructor
53 {
54 this->ptr = std::exchange(dyingObj.ptr,nullptr);
55 this->block = std::exchange(dyingObj.block,nullptr);
56 /*
57 this->ptr = dyingObj.ptr; // share the underlying pointer
58 this->block = dyingObj.block;
59 dyingObj.ptr = nullptr; // clean the dying object
60 dyingObj.block = nullptr;
61 */
62 }
63
64 MySharedPtr &operator=(MySharedPtr &&dyingObj) // move assignment
65 {
66 __cleanup__(); // cleanup any existing data
67 this->ptr = std::exchange(dyingObj.ptr,nullptr);
68 this->block = std::exchange(dyingObj.block,nullptr);
69 /*
70 this->ptr = dyingObj.ptr; // share the underlying pointer
71 this->block = dyingObj.block;
72
73 dyingObj.ptr = nullptr; // clean the dying object
74 dyingObj.block = nullptr;
75 */
76 return *this;
77 }
78
79 int get_count() const {
80 return block->ref_count; // *this->refCount
81 }
82
83 T *get() const { return this->ptr; }
84
85 T *operator->() const { return this->ptr; }
86
87 T &operator*() const { return this->ptr; }
88
89 ~MySharedPtr() // destructor
90 {
91 __cleanup__();
92 }
93
94private:
95 void __cleanup__() {
96 if (block == nullptr)
97 return;
98 block->ref_count--;
99 if (block->ref_count.load() == 0) {
100 if (nullptr != ptr)
101 delete ptr;
102 delete block;
103 }
104 }
105};
106
107int main() {
108 MySharedPtr<int> val1(new int(3));
109 printf("%d\n", val1.get_count()); // 1
110 MySharedPtr<int> val2(val1); // copy ctor
111 printf("%d\n", val1.get_count()); // 2
112 printf("%d\n", val2.get_count()); // 2
113 auto val3 = val1; // copy assigment operator
114 printf("%d\n", val1.get_count()); // 3
115 printf("%d\n", val3.get_count()); // 3
116 auto val4 = std::move(val1); // move ctor operator
117 printf("%d\n", val4.get_count()); // 3
118 val4 = MySharedPtr<int>(new int(10));
119 printf("%d\n", val4.get_count()); // 1 , new object
120 printf("%d\n", val2.get_count()); // 2, val4 removed
121 // printf("%d\n", val2.get_count());
122
123 return 0;
124}
125
why prefer std::make_shared to std::shared_ptr (new T)
- 异常安全(new之后如果出现OOM会导致分配的内存没有释放)
- 对称性(new和delete最好成对出现)
- std::make_shared是一次分配T+controll block的内存,使用shared_ptr的话要分配两次。 一次是new T,一次是为control block分配内存