手动实现智能指针

声明:本文完成来自极客时间的吴咏炜的专栏文章,我纯粹记录

smart_ptr实现

智能指针并不神秘,其实就是RAII资源管理功能的自然展现而已。智能指针可以简化资源管理,从根本上消除资源(包括内存)泄漏的可能性。

template <typename T>
class smart_ptr {
public:
  explicit smart_ptr(T* ptr = nullptr) : ptr_(ptr) {}
  ~smart_ptr() { delete ptr_; }
  T* get() const { return ptr_; }
private:
  T* ptr_;
};

上面这个类可以完成智能指针最基本的功能:对超出作用域的对象进行释放。但目前这个smart_ptr的行为还是和智能指针有点差异的:

  • 不能用 * 运算符解引用
  • 不能用 -> 运算符指向对象成员
  • 不能像指正一样用在布尔表达式里

不能这些问题相当容易解决,加几个成员函数就可以:

template <typename T>
class smart_ptr {
public:
  ...
  T* get() const { return ptr_; }
  T& operator*() const { return *ptr_; }
  T* operator->() const { return ptr_; }
  operator bool() const { return ptr_; }
  ...
};

拷贝构造和赋值

拷贝构造和赋值,暂且简称为拷贝,这是个比较复杂的问题。关键不是怎么实现,而是我们如何去定义其行为。假设有下面代码:

smart_ptr<int> ptr1(new int(10));
smart_ptr<int> ptr2(ptr1);

对于第二行的行为,究竟应当让其在编译期发生错误呢,还是可以有一个更合理的行为?下面我们来分析一下各种可能性。

最简单的方法显然是禁止拷贝。我们可以使用下面的代码:

template <typename T>
class smart_ptr { 
  ...
  smart_ptr(const smart_ptr&) = delete; 
  smart_ptr& operator=(const smart_ptr&) = delete;  
  ...
};

禁用这两个函数非常简单,最重要的是解决了一种可能出错的情况。即第二行 smart_ptr<int> ptr2(ptr1); 在编译时不会报错,但在运行时却会有未定义的行为—由于会对同一内存释放两次,通常情况下会导致程序崩溃。

那是否可以考虑在拷贝只能指针时把对象拷贝一份?不行,通常人们不会这么用,因为使用只能指针的目的就是要减少对象的拷贝。何况,虽然指针类型T实际指向的可能是T的派生类,例如:shape类型的指针指向的通常是circle或者triangle之类的对象。但C++中并没有通用的方法可以通过基类的指针来构造一个子类的对象来。

那我们来试试在拷贝时转移指针的所有权吧,大致实现如下:

template <typename T>
class smart_ptr {
public:
  ...
  smart_ptr(smart_ptr& other) {
    ptr_ = other.release();
  } 
  smart_ptr& operator=(smart_ptr& rhs) {
    smart_ptr(rhs.ptr_).swap(*this);
    return *this;
  }  
  T* release() {
    T* ptr = ptr_;
    ptr_ = nullptr;
    return ptr;
  }
  void swap(smart_ptr& rhs) {
    using std::swap;
    swap(ptr_, rhs.ptr_);
  }
  ...
};

在拷贝函数中,通过调用other的release方法来释放它对指针的所有权。在赋值函数中,则通过拷贝构造一个临时对象并调用swap来交换对指针的所有权。实际上并不复杂。

如果你学到的赋值函数可能还有一个类似 if (this != &rhs) 的判断,但这种用法其实更啰嗦,而且异常安全性不够好—如果在赋值过程中发生异常的话,this对象的内容可能已经被部分破坏了,对象不再处于一个完整的状态。先析构原对象,在赋值,这里可以参考:https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom/3279550#3279550

目前这种惯用法则保证了强异常安全性:赋值分为拷贝构造和交换两步,异常只可能在第一步发生;而第一步如果发生异常的话,this对象完全不受任何影响。

上面给出的语义本质上是C++98的auto_ptr的定义。但其实上面实现最大的问题是,它的行为会让成员非常容易犯错。一不小心把它传递给另外一个smart_ptr,你就不再拥有这个对象了。所以auto_ptr在C++17时已经被正式从C++标准里删除了。

“移动”指针

下面使用“移动”来改善smart_ptr的行为,我们需要对代码做两处小修改:

template <typename T>
class smart_ptr {
public:
  ...
  smart_ptr(smart_ptr&& other) {
    ptr_ = other.release();
  } 
  smart_ptr& operator=(smart_ptr rhs) {
    rhs.swap(*this);
    return *this;
  }  
  ...
};

上面改了两个地方:

  • 把拷贝构造函数中的参数类型 smart_ptr& 改为 smart_ptr&&,现在它成了移动构造函数
  • 把赋值函数中的参数类型 smart_ptr& 改为 smart_ptr,在构造参数时直接生成新的智能指针,从而不需要在函数体中构造临时对象。现在赋值函数的行为是移动还是拷贝,完全依赖于构造参数时走的是移动构造函数拷贝构造。

根据C++的规则,如果我们提供了移动构造函数而没有手动提供拷贝构造函数,那后者自动被禁用。于是我们自然得到以下结果:

smart_ptr<int> ptr1(new int(10));
smart_ptr<int> ptr2(ptr1);             // 编译出错
smart_ptr<int> ptr3;
ptr3 = ptr1;                           // 编译出错
ptr3 = std::move(ptr1);                // OK,可以
smart_ptr<int> ptr4(std::move(ptr3));  // OK,可以

这就自然多了。这也是C++11的 unique_ptr 的基本行为。

子类指针向基类指针的转换

在C++中,一个子类指针是可以隐式转换成基类指针的,例如:一个 circle* 可以隐式转换成 shape* 的,但上面的 smart_ptr<circle> 却不能自动转换成 smart_ptr<shape>。这显然不够“自然”。

不过,只需要额外加一个模板代码,就能实现这一行为。在我们目前给出的实现里,只需要增加一个构造函数即可—这也算是我们让赋值函数利用构造函数的好处了。

template <typename U>
smart_ptr(smart_ptr<U>&& other) {
  ptr_ = other.release();
}

这样我们自然而然利用了指针的转换特性,smart_ptr<circle> 可以移动给 smart_ptr<shape>,但不能移动给 smart_ptr<triangle>。不正确的转换会在代码编译时报错。

需要注意,上面这个构造函数不被编译器看做移动构造函数,因而不能自动触发删除拷贝构造函数的行为。如果我们想删除代码重复、删除移动构造函数的话,就需要把拷贝构造函数标记成 = delete 了。不过,更通用的方式仍然是同时定义标准的拷贝/移动构造函数和所需的模板构造函数。下面的引用计数智能指针我们就需要这么做。

至于非隐式的转换,因为本来就是要写特殊的转换函数的,我们留在这一讲的最后讨论。

引用计数

unique_ptr 算是一种较为安全的智能指针了。但是,一个对象只能被单个 unique_ptr 所拥有,这显然不能满足所有使用场景的需求。一种常见的情况是,多个智能指针同时拥有一个对象,当它们全部失效时,这个对象同时会被删除。这就是 shared_ptr 了。

unique_ptr 和 shared_ptr 的主要区别如下图所示:

 多个不同的 shared_ptr 不仅可以共享一个对象,在共享同一个对象时也需要同时共享同一个计数。当最后一个指向对象(和共享计数)的 shared_ptr 析构时,它需要删除对象和共享计数。我们下面就来实现一下。

我们先来写出共享计数的接口:

class shared_count {
public:
  shared_count();
  void add_count();
  long reduce_count();
  long get_count() const;
};

这个shared_count类除构造函数之外有三个方法:一个增加计数,一个减少计数,一个获取计数。注意上面的接口增加计数不需要返回计数值;但较少计数时需要返回计数值,以供调用者判断是否它是最后一个指向共享计数的 shared_ptr 了。由于真正多线程安全版本需要用到目前还没学到的知识,目前我们先实现一个简单化的版本:

class shared_count {
public:
  shared_count() : count_(1) {}
  void add_count() { ++count_; }
  long reduce_count() { return --count_; }
  long get_count() const { return count_; }
private:
  long count_;
};

现在我们可以实现引用计数智能指针了。首先是构造函数、析构函数和私有成员变量:

template <typename T>
class smart_ptr {
public:
  explicit smart_ptr(T* ptr = nullptr) : ptr_(ptr) {
    if (ptr) {
      shared_count_ = new shared_count();
    }
  }
  ~smart_ptr() {
    if (ptr_ && !shared_count_->reduce_count()) {
      delete ptr_;
      delete shared_count_;
    }
  }
private:
  T* ptr_;
  shared_count* shared_count_;
};

构造函数跟之前的主要不同点是会构造一个 shared_count 出来。析构函数在看到 ptr_ 非空时(此时根据代码逻辑,shared_count_也必然是非空),需要对引用计数减一,并在引用计数降到零时彻底删除对象和共享计数。原理就是这样,不复杂。

当然,我们还有些细节要处理。为了方便实现赋值(以及一些惯用法),我们需要一个新的 swap 成员函数:

void swap(smart_ptr& rhs) {
  using std::swap;
  swap(ptr_, rhs.ptr_);
  swap(shared_count_, rhs.shared_count_);
}

赋值函数可以跟之前一样,保持不变,但拷贝构造和移动构造函数需要更新一下:

template <typename T>
class smart_ptr {
public:
  ...
  smart_ptr(const smart_ptr& other) {
    ptr_ = other.ptr_;
    if (ptr_) {
      other.shared_count_->add_count();
      shared_count_ = other.shared_count_;
    }
  }
  template <typename U>
  smart_ptr(const smart_ptr<U>& other) {
    ptr_ = other.ptr_;
    if (ptr_) {
      other.shared_count_->add_count();
      shared_count_ = other.shared_count_;
    }
  }
  template <typename U>
  smart_ptr(smart_ptr<U>&& other) {
    ptr_ = other.ptr_;
    if (ptr_) {
      shared_count_ = other.shared_count_;
      other.ptr_ = nullptr;
    }
  }
  ...
};

除赋值指针之外,对于拷贝构造的情况,我们需要在指针非空时把引用计数加一,并复制共享计数的指针。对于移动构造的情况,我们不需要调用引用计数,直接把 other.ptr_ 置为空,认为 other 不再指向该共享对象即可。

不过,上面的代码有个问题:它不能正确编译,编译器会报错,像:

error: 'circle* smart_ptr<circle>::ptr_' is private within this context

错误原因是模板的各个实例间并不天然就有 friend 关系,因而不能互私访问私有成员 ptr_ 和 shared_count_。我们需要在 smart_ptr 的定义中显示声明:

template <typename U>
friend class smart_ptr;

此外,我们之前的实现(类似于单一所有权的 unique_ptr )中用 release 来手动释放所有权。在目前的计数实现中,它就不合适了,应当删除。但我们要加一个对调试非常有用的函数,返回引用计数值。定义如下:

long use_count() const { 
  return ptr_ ? shared_count_->get_count() : 0;
}

这就差不多是一个比较完整的引用计数智能指针的实现了。我们可以用下面的代码来验证一下它的功能正常:

class shape {
public:
  virtual ~shape() {}
};

class circle : public shape {
public:
  ~circle() { puts("~circle"); }
};

void test() {
    smart_ptr<circle> ptr1(new circle());
    printf("use count of ptr1 is %ld\n", ptr1.use_count());
    smart_ptr<shape> ptr2;
    printf("use count of ptr2 is %ld\n", ptr2.use_count());
    ptr2 = ptr1;
    printf("use count of ptr2 is %ld\n", ptr2.use_count());
    if (ptr1) {
        puts("ptr1 is not empty");
    }
}

int main() {
    test();
    system("pause");
    return 0;
}

这段代码的运行结果是:

use count of ptr1 is 1
use count of ptr2 is 0
use count of ptr2 is 2
ptr1 is not empty
~circle

上面我们可以看到引用计数的变化,以及最后对象被成功删除。

指针类型转换

对应于 C++ 里的不同的类型强制转换:

  • static_cast
  • const_cast
  • dynamic_cast
  • reinterpret_cast

 智能指针需要类似的函数模板。实现本身并不复杂,但为了实现这些转换,我们需要添加构造函数,允许对智能指针内部的指针对象赋值时,使用一个现有的智能指针的共享计数。如下所示:

class smart_ptr {
public:
  ...
  template <typename U>
  smart_ptr(const smart_ptr<U>& other, T* ptr) {
    ptr_ = ptr;
    if (ptr_) {
      other.shared_count_->add_count();
      shared_count_ = other.shared_count_;
    }
  }
  ...
};

这样我们就可以实现转换所需的函数模板了,下面实现一个 dynamic_pointer_cast 来示例一下:

template <typename T, typename U>
smart_ptr<T> dynamic_pointer_cast(const smart_ptr<U>& other) {
  T* ptr = dynamic_cast<T*>(other.get());
  return smart_ptr<T>(other, ptr);
}

在前面的验证代码后面我们加上:

  smart_ptr<circle> ptr3 = dynamic_pointer_cast<circle>(ptr2);
  printf("use count of ptr3 is %ld\n", ptr3.use_count());

编译正常通过,同时输出下面结果

use count of ptr3 is 3

最后对象仍能够被正确的删除,说明我们的实现是正确的。

代码列表

最后给出完整的smart_ptr代码列表:

class shared_count {
public:
  shared_count() noexcept
   : count_(1) {}
  void add_count() noexcept
  { 
    ++count_; 
  }
  long reduce_count() noexcept
  { 
    return --count_; 
  }
  long get_count() const noexcept
  { 
    return count_; 
  }
  
private:
  long count_;
};

template <typename T>
class smart_ptr {
public:
  template <typename U>
  friend class smart_ptr;

  explicit smart_ptr(T* ptr = nullptr) : ptr_(ptr) {
    if (ptr) {
      shared_count_ = new shared_count();
    }
  }
  ~smart_ptr() {
    if (ptr_ && !shared_count_->reduce_count()) {
      delete ptr_;
      delete shared_count_;
    }
  }
  smart_ptr(const smart_ptr& other) {
    ptr_ = other.ptr_;
    if (ptr_) {
      other.shared_count_->add_count();
      shared_count_ = other.shared_count_;
    }
  }
  template <typename U>
  smart_ptr(const smart_ptr<U>& other) noexcept
  {
    ptr_ = other.ptr_;
    if (ptr_) {
      other.shared_count_->add_count();
      shared_count_ = other.shared_count_;
    }
  }
  template <typename U>
  smart_ptr(smart_ptr<U>&& other) noexcept
  {
    ptr_ = other.ptr_;
    if (ptr_) {
      shared_count_ = other.shared_count_;
      other.ptr_ = nullptr;
    }
  }
  template <typename U>
  smart_ptr(const smart_ptr<U>& other, T* ptr) noexcept
  {
    ptr_ = ptr;
    if (ptr_) {
      other.shared_count_->add_count();
      shared_count_ = other.shared_count_;
    }
  }
  smart_ptr& operator=(smart_ptr rhs) noexcept
  {
    rhs.swap(*this);
    return *this;
  }  
  T* get() const noexcept
  { 
    return ptr_; 
  }
  T& operator*() const noexcept
  { 
    return *ptr_; 
  }
  T* operator->() const noexcept
  { 
    return ptr_; 
  }
  operator bool() const noexcept
  { 
    return ptr_; 
  }
  void swap(smart_ptr& rhs) noexcept
  {
    using std::swap;
    swap(ptr_, rhs.ptr_);
    swap(shared_count_, rhs.shared_count_);
  }
  long use_count() const noexcept
  { 
    return ptr_ ? shared_count_->get_count() : 0;
  }

private:
  T* ptr_;
  shared_count* shared_count_;
};

template <typename T>
void swap(smart_ptr<T>& lhs, smart_ptr<T>& rhs) noexcept
{
  lhs.swap(rhs);
}

template <typename T, typename U>
smart_ptr<T> dynamic_pointer_cast(const smart_ptr<U>& other) noexcept
{
  T* ptr = dynamic_cast<T*>(other.get());
  return smart_ptr<T>(other, ptr);
}
 
template <typename T, typename U>
smart_ptr<T> static_pointer_cast(const smart_ptr<U>& other) noexcept
{
  T* ptr = static_cast<T*>(other.get());
  return smart_ptr<T>(other, ptr);
}

template <typename T, typename U>
smart_ptr<T> const_pointer_cast(const smart_ptr<U>& other) noexcept
{
  T* ptr = const_cast<T*>(other.get());
  return smart_ptr<T>(other, ptr);
}

template <typename T, typename U>
smart_ptr<T> reinterpret_pointer_cast(const smart_ptr<U>& other) noexcept
{
  T* ptr = reinterpret_cast<T*>(other.get());
  return smart_ptr<T>(other, ptr);
}
posted @ 2019-12-19 11:58  evenleo  阅读(650)  评论(0)    收藏  举报