std::shared_ptr 学习笔记

Overview

概述

std::shared_ptr是智能指针的一种,在modern c++中被广泛使用(甚至滥用)

虽然天天使用,但是有些细节还不是100%清楚,因此来整理一下 为了方便表述,下文只写shared_ptr,不在写std的namespace.

组成

shared_ptr的实现中,成员通常由两部分组成。一个是所涵盖对象的指针,一个是control block 的指针

control block

Tip

最重要的是,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.

这里面有几点值得强调:

  1. 两个引用计数都是atomic的。
  2. weak_ptr是为了解决循环引用
  3. type-erased是什么? 后面会介绍

线程安全

shared_ptr的线程安全性快成c++面试的top10经典八股文了

Tip

简单说,shared_ptr<T>的引用计数的实现是线程安全的(通常是两个atomic变量),但是对于T的操作不是线程安全的*

type erasure(erase的名词形式)

也称为type-earsed

是指在程序运行的时候,不需要知道具体的类型。与之相反的是 type-passing semantics

Tip

实现 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

这里值得注意的有两点.

Tip
  1. shared_ptr的class template parameter只有一个T,但是构造函数有另外个member tempalte parameter U. 也就是说shared_ptr的deleter不属于其shared_ptr类型的一部分。 这样做增加了灵活性(同样的T可以使用不同的deleter),但是也增加了额外的开销(需要存储deleter,并且如果没有使用std::make_shard的话需要额外的空间分配). 但是shared_ptr由于存在额外的引用计数,本来就要有额外的开销

  2. 与之不同的是,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 来简化移动语义的实现

最重要的一点仍然是

Tip

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)

  1. 异常安全(new之后如果出现OOM会导致分配的内存没有释放)
  2. 对称性(new和delete最好成对出现)
  3. std::make_shared是一次分配T+controll block的内存,使用shared_ptr的话要分配两次。 一次是new T,一次是为control block分配内存

参考链接