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,只有一个简单的引用计数
最重要的一点仍然是
ControlBlock是要动态分配出来的一个指针,不然无法在不同object之间共享引用计数
1
2#include <atomic>
3#include <cstdio>
4#include <ios>
5#include <iostream>
6
7struct ControlBlock {
8 std::atomic<int> ref_count;
9 ControlBlock(int cnt) { ref_count.store(cnt); }
10};
11
12template <class T> class MySharedPtr {
13private:
14 T *ptr = nullptr;
15 ControlBlock *block = nullptr;
16
17public:
18 MySharedPtr()
19 : ptr(nullptr), block(new ControlBlock(0)) // default constructor
20 {}
21
22 MySharedPtr(T *ptr)
23 : ptr(ptr), block(new ControlBlock(1)) // constructor
24 {}
25
26 /*** Copy Semantics ***/
27 MySharedPtr(const MySharedPtr &obj) // copy constructor
28 {
29 this->ptr = obj.ptr; // share the underlying pointer
30 this->block = obj.block;
31 if (nullptr != obj.ptr) {
32 (*this->block)
33 .ref_count++; // if the pointer is not null, increment the refCount
34 }
35 }
36
37 MySharedPtr &operator=(const MySharedPtr &obj) // copy assignment
38 {
39 __cleanup__(); // cleanup any existing data
40
41 // Assign incoming object's data to this object
42 this->ptr = obj.ptr; // share the underlying pointer
43 this->refCount = obj.refCount;
44 if (nullptr != obj.ptr) {
45 (*this->refCount)++; // if the pointer is not null, increment the refCount
46 }
47 return *this;
48 }
49
50 /*** Move Semantics ***/
51 MySharedPtr(MySharedPtr &&dyingObj) // move constructor
52 {
53 this->ptr = dyingObj.ptr; // share the underlying pointer
54 this->block = dyingObj.block;
55
56 dyingObj.ptr = nullptr; // clean the dying object
57 dyingObj.block = nullptr;
58 }
59
60 MySharedPtr &operator=(MySharedPtr &&dyingObj) // move assignment
61 {
62 __cleanup__(); // cleanup any existing data
63 this->ptr = dyingObj.ptr; // share the underlying pointer
64 this->block = dyingObj.block;
65
66 dyingObj.ptr = nullptr; // clean the dying object
67 dyingObj.block = nullptr;
68 return *this;
69 }
70
71 int get_count() const {
72 return block->ref_count; // *this->refCount
73 }
74
75 T *get() const { return this->ptr; }
76
77 T *operator->() const { return this->ptr; }
78
79 T &operator*() const { return this->ptr; }
80
81 ~MySharedPtr() // destructor
82 {
83 __cleanup__();
84 }
85
86private:
87 void __cleanup__() {
88 if (block == nullptr)
89 return;
90 block->ref_count--;
91 if (block->ref_count.load() == 0) {
92 if (nullptr != ptr)
93 delete ptr;
94 delete block;
95 }
96 }
97};
98
99int main() {
100 MySharedPtr<int> val1(new int(3));
101 printf("%d\n", val1.get_count()); // 1
102 MySharedPtr<int> val2(val1); // copy ctor
103 printf("%d\n", val1.get_count()); // 2
104 printf("%d\n", val2.get_count()); // 2
105 auto val3 = val1; // copy assigment operator
106 printf("%d\n", val1.get_count()); // 3
107 printf("%d\n", val3.get_count()); // 3
108 auto val4 = std::move(val1); // move ctor operator
109 printf("%d\n", val4.get_count()); // 3
110 val4 = MySharedPtr<int>(new int(10));
111 printf("%d\n", val4.get_count()); // 1 , new object
112 printf("%d\n", val2.get_count()); // 2, val4 removed
113 // printf("%d\n", val2.get_count());
114
115 return 0;
116}
117
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分配内存