实现C++智能指针

对象切片一文中,提到可使用充当智能指针的类shape_wrapper,可以简化资源的管理,从根本上消除资源(包括内存)泄漏的可能性,本节来看下如何将shape_wrapper改造成一个完整的智能指针。

先来看下这个类:

class shape_wrapper {
public:
  explicit shape_wrapper(
    shape* ptr = nullptr)
    : ptr_(ptr) {}
  ~shape_wrapper()
  {
    delete ptr_;
  }
  shape* get() const { return ptr_; }

private:
  shape* ptr_;
};

这个类完成了智能指针的基本功能:对超出作用域的对象进行释放。但是还缺点东西:

  • 这个类只适用于shape类

  • 该类对象的行为不够向指针

  • 拷贝该类对象会引发程序行为异常

模板化和易用性

要把这个类能够包装任意类型的指针,就需要把他变成一个类模板。

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_;
};

和 shape_wrapper 比较一下,就是在开头增加模板声明 template ,然后把代码中的 shape 替换成模板参数 T 而已,使用上也很简单,就把原来shape_wrapper改成smart_ptr<shape>就行

目前这个smart_ptr的行为还和指针有点差异:

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

这也很好解决,加几个成员函数就可以:

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

拷贝构造和赋值

暂且称拷贝构造和赋值简称为拷贝。

假设有下面的代码:

smart_ptr<shape> ptr1{create_shape(shape_type::circle)};
smart_ptr<shape> ptr2{ptr1};

对于第二行,究竟应当让编译时发生错误,还是可以有个更合理的行为呢?

  • 最简单的情况显然就是禁止拷贝
template <typename T>
class smart_ptr {
  …
  smart_ptr(const smart_ptr&)
    = delete;
  smart_ptr& operator=(const smart_ptr&)
    = delete;
  …
};

禁用这两个函数非常简单,但却解决了一种可能出错的情况。否则,smart_ptr<shape>ptr2{ptr1};在编译时不会出错,但在运行时却会有未定义行为 -- 由于会对同一内存释放两次,通常情况下会导致程序崩溃

  • 那是不是可以考虑在拷贝智能指针时把对象拷贝一份?不行
    因为使用智能指针的目的就是减少对象的拷贝,何况,我们的指针类型是shape,但实际指向的却应该是circletriangle之类的对象。C++里也没有像Java的clone方法的约定。
    一般而言,并没有通用的方法可以通过基类的指针来构造出一个子类的 对象。

  • 要么试试在拷贝时转移智能指针的所有权?
    大致实现如下:

template <typename T>
class smart_ptr {
  …
  smart_ptr(smart_ptr& other)
  {
    ptr_ = other.release();
  }
  smart_ptr& operator=(smart_ptr& rhs)
  {
    smart_ptr(rhs).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 对象的内容可能已经被部分破坏了,对象不再处于一个完整的状态。

上面代码的这种惯用法则保证了强异常安全性: 赋值分为拷贝构造交换两步,异常只可能在第一步发生;而第一步如果发生异常的话,this 对象完全不受任何影响。无论拷贝构造成功与否,结果只有赋值成功和赋值没有效果两种状态,而不会发生因为赋值破坏了当前对象这种场景。

上面实现的最大问题在于,它的行为会让程序员非常容易犯错。一不小心把它传递给另一个smart_ptr,就不再拥有这个对象....

"移动"指针?

先来简单看下smart_ptr如何使用"移动"来改善其行为的。

template <typename T>
class smart_ptr {
  …
  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++ 的规则,如果我提供了移动构造函数而没有手动提供拷贝构造函数,那后者自动被禁用(记住,C++ 里那些复杂的规则也是为方便编程而设立的)。于是,我们自然地得到了以下结果:

smart_ptr<shape> ptr1{create_shape(shape_type::circle)};
smart_ptr<shape> ptr2{ptr1};             // 编译出错
smart_ptr<shape> ptr3;
ptr3 = ptr1;                             // 编译出错
ptr3 = std::move(ptr1);                  // OK,可以
smart_ptr<shape> ptr4{std::move(ptr3)};  // OK,可以

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

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

一个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 的主要区别如下图所示:
image

多个不同的 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_);
  }

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

  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 不再指向该共享对象即可。

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

fatal error: ‘ptr_’ is a private member of ‘smart_ptr’

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

  template <typename U>
  friend class smart_ptr;

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

  long use_count() const
  {
    if (ptr_) {
      return shared_count_
        ->get_count();
    } else {
      return 0;
    }
  }

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

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

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

int main()
{
  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 was %ld\n",
         ptr2.use_count());
  ptr2 = ptr1;
  printf("use count of ptr2 is now %ld\n",
         ptr2.use_count());
  if (ptr1) {
    puts("ptr1 is not empty");
  }
}

这段代码的运行结果是:

use count of ptr1 is 1
use count of ptr2 was 0
use count of ptr2 is now 2
ptr1 is not empty
~circle()

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

指针类型转换

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

  • static_cast

  • reinterpret_cast

  • const_cast

  • dynamic_cast

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

  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 代码列表:

#include <utility>  // std::swap

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_;
  }
  long use_count() const noexcept
  {
    if (ptr_) {
      return shared_count_
        ->get_count();
    } else {
      return 0;
    }
  }
  void swap(smart_ptr& rhs) noexcept
  {
    using std::swap;
    swap(ptr_, rhs.ptr_);
    swap(shared_count_,
         rhs.shared_count_);
  }

  T& operator*() const noexcept
  {
    return *ptr_;
  }
  T* operator->() const noexcept
  {
    return ptr_;
  }
  operator bool() const noexcept
  {
    return ptr_;
  }

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> 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> reinterpret_pointer_cast(
  const smart_ptr<U>& other) noexcept
{
  T* ptr = reinterpret_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> dynamic_pointer_cast(
  const smart_ptr<U>& other) noexcept
{
  T* ptr = dynamic_cast<T*>(other.get());
  return smart_ptr<T>(other, ptr);
}

会发现我在代码里加了不少 noexcept。这对这个智能指针在它的目标场景能正确使用是十分必要的

posted @ 2022-09-28 08:14  牛犁heart  阅读(121)  评论(0编辑  收藏  举报