02 | 自己动手,实现C++的智能指针
第一步:针对单独类型的模板
为了完成智能指针首先第一步的想法。
class shape_wrapper { public: explicit shape_wrapper( shape* ptr = nullptr) : ptr_(ptr) {} ~shape_wrapper() { delete ptr_; } shape* get() const { return ptr_; } private: shape* ptr_; };
这个类可以完成智能指针的最基本的功能:对超出作用域的对象进行释放。
但是
1.这个类只适用于 shape 类
2.该类对象的行为不够像指针
3.拷贝该类对象会引发程序行为异常
要让这个类能够包装任意类型的指针,我们需要把它变成一个类模板
第二步:变成类模板
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_; };
解决像不像指针的问题:
它不能用 * 运算符解引用
它不能用 -> 运算符指向对象成员
它不能像指针一样用在布尔表达式里
template <typename T> class smart_ptr { public: … T& operator*() const { return *ptr_; } T* operator->() const { return ptr_; } operator bool() const { return ptr_; } }
第三步:拷贝构造和赋值
第一种粗鲁的方式
解决了会对同一内存释放两次,通常情况下会导致程序崩溃的问题。
template <typename T> class smart_ptr { … smart_ptr(const smart_ptr&) = delete; smart_ptr& operator=(const smart_ptr&) = delete; … };
第二种移动所有权
大致实现如下
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 对象完全不受任何影响。无论拷贝构造成功与否,结果只有赋值成功和赋值没有效果两种状态,而不会发生因为赋值破坏了当前对象这种场景。
还是不够完善,没有支持移动语义,于是继续进行修改如下
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; } … };
修改的地方:
1.把拷贝构造函数中的参数类型 smart_ptr& 改成了 smart_ptr&&;现在它成了移动构造函数。
2.把赋值函数中的参数类型 smart_ptr& 改成了 smart_ptr,在构造参数时直接生成新的智能指针,从而不再需要在函数体中构造临时对象。现在赋值函数的行为是移动还是拷贝,完全依赖于构造参数时走的是移动构造还是拷贝构造。
根据 C++ 的规则,如果我提供了移动构造函数而没有手动提供拷贝构造函数,那后者自动被禁用
到这里我们完成了一个C++11 的 unique_ptr 的基本行为。
第四步:unique_ptr的完善:子类指针向基类指针的转换
直接上代码,利用内置类型自己的判断来辅助完成转换
template <typename U> smart_ptr(smart_ptr<U>&& other) { ptr_ = other.release(); }
到这里我们完成了一个完整的unique_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; };
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<circle>’
错误原因是模板的各个实例间并不天然就有 friend 关系,因而不能互访私有成员 ptr_ 和 shared_count_。我们需要在 smart_ptr 的定义中显式声明:
template <typename U> friend class smart_ptr;
第六步:指针类型转换
对应于 C++ 里的不同的类型强制转换:我们能不能让我们的智能指针同样支持这种转换?
static_cast reinterpret_cast const_cast dynamic_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); //再根据具体的指针来构造目标,借助构造函数来完成 }
补充一个构造函数
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_; } }
总结代码
#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;//模板类自身friend 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;// 设置为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 // 通过成员函数swap内部调用 std内部的标准swap { 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。这对这个智能指针在它的目标场景能正确使用是十分必要的。我们会在下面的几讲里回到这个话题。