1. 第一步,把wrapper改造为模板类
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_;
}
2. 第二步:仿指针行为
T& operator*() const {return *ptr_;}
T* operator->() const {return ptr_;}
operator bool() const {return ptr_;}
3. 第三步:实现资源移动
3.1. 策略一:禁止拷贝
使用后,一个空间只会被一个smart_ptr指向。
实现方法:
smart_ptr(const smart_ptr &) = delete;
smart_ptr& operator=(const smart_ptr&) = delete;
3.2. 策略二:拷贝时自动移动资源
使用后,一个空间只会被一个smart_ptr指向。
可以拷贝。拷贝后,新smart_ptr指向空间,旧smart_ptr失效。
T* release()
{
T* ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
smart_ptr(smart_ptr & other)
{
ptr_ = other.release();
}
void swap(smart &rhs)
{
using std::swap;
swap(ptr_, rhs.ptr_);
}
smart_ptr& operator=(smart_ptr &rhs)
{
smart_ptr(rhs).swap(*this);
return *this;
}
说明:用拷贝构造 + swap实现拷贝赋值是一种惯用法,用于保证异常安全
3.2.1. 策略二存在的问题
template <typename T>
void print_addr(smart_ptr<ptr>)
{
....
}
执行print_addr(ptr);
之后ptr会变成空指针。
因为ptr的内容移动到了print_addr
的栈上,而函数返回时print_addr
的栈又被释放掉了。
策略二是auto_ptr
的行为,它有风险,因此在C++ 19中被取消掉了。
3.3. 策略三:利用右值引用,区分拷贝和移动
smart_ptr(smart_ptr&& other)
{
ptr_ = other.release();
}
smart_ptr& operator=(smart_ptr rhs)
{
rhs.swap(*this);
return *this;
}
说明:smart_ptr(smart_ptr&& other)
是一个移动构造函数operator=
的形参smart_ptr rh
,根据实参选择调用移动构造还是拷贝构造。这样不需要分别实现移动赋值和拷贝赋值。
但此处没有实现拷贝构造,因此也不存在拷贝赋值。
3.3.1. 策略三的使用方法
smart_ptr<T> ptr1{create()};
{}
内的是一个右值,因此调用移动构造。smart_ptr<T> ptr2{ptr1};
ptr1
是一个左值,因此调用拷贝构造函数。但没有实现拷贝构造函数,因此编译出错。smart_ptr<T> ptr3; ptr3 = ptr1;
ptr1
是一个左值,调用拷贝赋值,编译出错。ptr3 = std::move(ptr1)
将左值ptr1
转为右值,调用移动赋值。smart_ptr<T> ptr4{std::move(ptr3};
左值转为右值,调用移动构造。
3.3.2. 说明
策略三是unique_ptr
的行为。
策略三如果需要实现子类指针向父类指针的自动转换,需要再加一个函数
template <typename U>
smart_ptr(smart_ptr<U> &&ohter)
{
ptr_ = other.release();
}
3.4. 策略四:共享和计数
多个指针指向同一内存,且指向同一个计数器。
第一个创建空间的指针同时创建和初始化计数器。
计数器减为0时释放空间。
3.4.1. 实现
策略四需要提供以下几种构造函数:
// 模板拷贝构造函数
template <typename U>
smart_ptr(const smart_ptr<U> & other)
// 模板移动构造
template<typename U>
smart_ptr(const smart_ptr<U> && other)
// 拷贝构造函数
smart_ptr(const smart_ptr& other)
说明:
如果无显式的拷贝构造函数,编译器会提供一个错误的。如果仅提供了模板拷贝构造函数,编译器不会认为是有显式的拷贝构造函数。
如果有显式的拷贝构造函数,编译器就不会提供移动构造函数,那么所有移动构造会调用上面的“模板移动构造”,不会出错。
3.5. 实现对象移动的总结
- 分别实现拷贝构造和移动构造,除非这个对象只能移动不能拷贝。如
unique_ptr
无拷贝只移动,参考策略二unique_ptr
。策略一auto_pr
是反而教材。 - 对象应有swap成员函数,用于与另一个对象快速交换成员。
对象的名空间下有自由swap函数,调用成员函数的swap实现交换。 - 实现一份能用的operator=,可自动适配移动赋值和拷贝赋值。
- 移动函数和swap函数不能抛异常,noexcept