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,只有一个简单的引用计数

最重要的一点仍然是

Tip

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)

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

参考链接