首先,以下代码可以完成智能指针的最基本功能:对超出作用域的对象进行释放。
1 | template <typename T> |
这里的
explicit
的作用是禁用隐式转换,必须显式地调用构造函数。
但它缺少了一些东西:
- 该类对象的行为不够像指针(通过
->
和*
的方式进行操作) - 拷贝该类对象会引发程序行为异常
对于第一点比较容易解决,增加几个成员函数就可以:
1 | T &operator*() const { return *ptr_; } |
拷贝构造和赋值
而对于拷贝构造和赋值,我们就需要思考该如何定义其行为。假设对于:
1 | smart_ptr<shape> ptr2{ptr1}; |
在第二行中,我们应该如何定义这里的行为?
在拷贝智能指针时把对象也拷贝一份?通常不会这么做,因为使用智能指针的目的就是要减少对象的拷贝。
在拷贝时转移指针的所有权?
1 | template <typename T>` |
这里有一个优化细节,利用
using std::swap
和无命名空间限定的swap
调用,来启用一种能让编译器通过 ADL 自动发现并使用最优swap
函数的编程模式。如果找不到最优的,它也会回退到使用标准的std::swap
。
并且在赋值函数的实现中保证了强异常安全性:赋值分为拷贝构造和交换两步,异常只可能在第一步发生;而如果第一步发生了异常,那么也不会对当前的*this
产生影响。
实际上,这就是 C++98 的 auto_ptr
的定义,它在 C++17 时已经被删除了。它的问题是如果一不小心把一个smart_ptr
传递给了另一个,那你就不再拥有原先的smart_ptr
了。
“移动”指针
接下来,可以尝试用“移动”来改善一下smart_ptr
的行为:
1 | smart_ptr(smart_ptr &&other) { ptr_ = other.release(); } |
- 把拷贝构造函数中的参数从
smart_ptr&
(引用)改为了smart_ptr&&
(右值引用),现在它变成了移动构造函数; - 把赋值函数中的参数类型从
smart_ptr&
改为了smart_ptr
,也就是说现在在构造参数时就会直接生成一个临时的智能指针,不再需要在函数体中构造临时对象。
根据 C++的规则,这里提供了移动构造函数,而没有显式提供拷贝构造函数,那么后者将会被自动禁用。
1 | smart_ptr ptr1{new int(42)}; |
子类指针向基类指针的转换
我们知道,一个circle*
是可以隐式转换为shape*
的,但是现在我们的smart_ptr
却没办法做smart_ptr<circle>
到smart_ptr<shape>
这样的转换,行为还是不够“自然”。
不过,只需要增加一个构造函数,就能够实现这一行为:
1 | template <typename U> |
对于不正确的转换则会在代码编译时直接报错。
需要注意,上面这个构造函数不会被编译器看作移动构造函数,因此不能自动触发删除拷贝构造函数的行为。
引用计数
unique_ptr
只能指向一个对象,这显然不能满足所有使用场合的需求。更常见的情况是,多个智能指针同时拥有一个对象,当它们全部都失效时,这个对象也同时会被删除,这也就是shared_ptr
了。
多个shared_ptr
在共享同一对象时也需要同时共享同一个计数。
1 | class shared_count |
增加计数的方法不需要返回计数值;但减少计数时需要返回计数值,以供调用者判断是否它已经是最后一个指向共享计数的shared_ptr
了。
现在我们可以实现带引用计数的智能指针了。
1 | template <typename T> |
构造函数会同步构造一个shared_count
出来,析构函数则会在ptr_
非空时,将引用数减一,并在引用数降到零时删除对象和共享计数。
1 | smart_ptr(smart_ptr &&other) |
对于拷贝构造的情况,则需要同步将引用计数加一;对于移动构造的情况,则将other
的指向去掉即可。
不过对于上面的代码而言有个问题,在以下情况会编译报错:
1 | smart_ptr<circle> p(new int(42)); |
因为在跨类型的实例之间不天然就有friend
关系,因此不能互相访问私有成员ptr_
和shared_count_
,我们需要在smart_ptr
中显式说明:
1 | template <typename U> |
别忘了在swap
中补充对shared_count_
的交换:
1 | void swap(smart_ptr &other) |
此外,之前的实现中用release
来手工释放所有权的形式在当前场景下就不太合适了,应当删除。但我们可以加一个对调试非常有用的方法,返回引用计数值,如下:
1 | long use_count() const |
接下来就可以验证下功能是否正常:
1 | int main() |
输出结果为:
1 | use count of ptr1 is 1 |
可以看到引用计数的变化,以及最后对象被成功删除。
指针类型转换
对应 C++中不同的强制类型转换,智能指针也需要实现类似的函数模板。需要注意的就是将智能指针内部的指针进行类型转换后,别忘了对引用计数的变更。
添加一个构造函数用于类型转换:
1 | template <typename U> |
实现一个dynamic_pointer_cast
作为示例,其它几个的逻辑都是一样的:
1 | template <typename T, typename U> |
验证:
1 | smart_ptr<circle> ptr3 = dynamic_pointer_cast<circle>(ptr2); |
输出结果:
1 | use count of ptr3 is 3 |