unique_ptr


在〈auto_ptr〉中,主要是认识自动管理动态配置对象的原理,c++ 98 的auto_ptr被废弃的原因显而易见,往往一个不小心,就忽略了资源被接管的问题,另一个问题是,它无法管理动态配置的连续空间,因为不会使用delete []来删除。

对于第一个问题,主要原因来自于复制时就会发生资源接管,既然如此,就禁止复制吧!这可以将复制构造函数与复制指定运算符删掉来达到,不过,实际上还是会需要转移资源权,那么就明确地定义释放资源与重置资源的方法;对于第二个问题,可以让使用者指定删除器,自行决定怎么删除资源。

实际上 C++ 11 的标准程序库在memory标头文件,定义有unique_ptr实现了以上的概念,不过试着自行实现个基本版本,是个不错的挑战,也能对unique_ptr有更多认识,那就来看个基本的版本吧!

#include <iostream>
#include<utility>
#include <functional>
using namespace std;

class Deleter {
public:
    template <typename T>
    void operator()(T* ptr) { delete ptr; }
};

// 默认的 D 类型是 Deleter
template<typename T, typename D = Deleter>
class UniquePtr {
    T* p;
    D del;

public:
    // 不能复制
    UniquePtr(const UniquePtr<T>&) = delete;
    UniquePtr<T>& operator=(const UniquePtr<T>&) = delete;

    UniquePtr() = default;

    // 每个 UniquePtr 有自己的 Deleter
    UniquePtr(T* p, const D &del = D()) : p(p), del(del) {}

    // 对于右值可以直接进行资源的移动
    UniquePtr(UniquePtr<T>&& uniquePtr) : p(uniquePtr.p), del(std::move(uniquePtr.del)) {
        uniquePtr.p = nullptr;
    }

    UniquePtr<T>& operator=(UniquePtr<T> &&uniquePtr) {
        if(this != &uniquePtr) {
            this->reset();
            this->p = uniquePtr.p;
            del = std::move(uniquePtr.del);
            uniquePtr.p = nullptr;
        }
        return *this;
    }

    ~UniquePtr() {
        del(this->p);
    }

    // 释放资源的管理权
    T* release() {
        T* r = this->p;
        this->p = nullptr;
        return r;
    }

    // 重设管理的资源
    void reset(T *p = nullptr) {
        del(this->p);
        this->p = p;
    }    

    // 令 UniquePtr 行为像个指针
    T& operator*() { return *(this->p); }
    T* operator->() { return this->p; }
};

...未完

来从实际的使用中认识这个实现:

...略

class Foo {
public:
    int n;
    Foo(int n) : n(n) {}
    ~Foo() {
        cout << n << " Foo deleted" << endl;
    }
};

int main() {
    UniquePtr<Foo> f1(new Foo(10)); 
    UniquePtr<Foo> f2(new Foo(20)); 

    f2.reset(f1.release());

    return 0;
}

因为无法复制了,在上例中,你不能UniquePtr<Foo> f2 = f1,或者是f2 = f1,因此不会隐含地就转移了资源的管理权,然而,可以透过release本身释放资源,f1.release()后不再管理原本的资源,资源的地址被返回,透过f2reset设定给f2f2原本的资源会被删除,管理的资源被设定为接收到的资源,透过releasereset,资源的转移得到了明确的语义。

因为无法复制了,你不能将UniquePtr实例作为实参传入函数;然而,这边看到了rvalue表达式与std::move的一个应用,当UniquePtr实例作为返回值时,虽然调用者会创建新的UniquePtr实例,然而因为实现了移动构造函数与移动指定运算符,被返回的UniquePtr实际上进行了资源的移动,结果就是,你可以从函数中返回UniquePtr实例。例如:

...

auto unique_foo(int n) {
    return UniquePtr<Foo>(new Foo(n)); 
}

int main() {
    auto foo = unique_foo(10); 
    cout << foo->n << endl;

    return 0;
}

这个范例的意思就是,既然自动管理资源了,就透过unique_foo避免使用new吧!如果要管理动态配置的连续空间呢?

...略

auto unique_arr(int len) {
    auto deleter = [](int *arr) { delete [] arr; };
    return UniquePtr<int, decltype(deleter)>(new int[len] {0}, deleter); 
}

int main() {
    auto arr = unique_arr(10); 
    cout << *arr << endl;

    return 0;
}

透过自定义的删除器,就可以指定如何删除动态配置的连续空间了,当然,这边实现的UniquePtr并不全面,因为没有重载下标运算符,因此无法如数组可以使用下标操作。

来看看标准程序库的unique_ptr怎么用吧!

#include <iostream>
#include<memory>
#include <functional>
using namespace std;

class Foo {
public:
    int n;
    Foo(int n) : n(n) {}
    ~Foo() {
        cout << n << " Foo deleted" << endl;
    }
};

int main() {
    unique_ptr<Foo> f1(new Foo(10)); 
    unique_ptr<Foo> f2(new Foo(20)); 

    f2.reset(f1.release());

    return 0;
}

C++ 11 时要以new创建unique_ptr,这是制定规范时的疏忽,从 C++ 14 开始,建议使用make_unique,这可以避免直接使用new

#include <iostream>
#include<memory>
#include <functional>
using namespace std;

class Foo {
public:
    int n;
    Foo(int n) : n(n) {}
    ~Foo() {
        cout << n << " Foo deleted" << endl;
    }
};

int main() {
    auto f1 = make_unique<Foo>(10); 
    auto f2 = make_unique<Foo>(20); 
    f2.reset(f1.release());

    return 0;
}

C++ 11 没有make_unique,不过可以自行实现:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

这个版本的make_unique指定的实参,都会用于构造实例,如果是动态配置连续空间呢?C++ 11 时,为此准备了另一个版本的unique_ptr,支持下标运算符,例如:

#include <iostream>
#include<memory>
using namespace std;

int main() {
    unique_ptr<int[]> arr(new int[3] {1, 2, 3});

    for(auto i = 0; i < 3; i++) {
        cout << arr[i] << endl;
    }

    return 0;
}

这个版本不用指定删除器,在unique_ptr生命周期结束时,会自动删除动态配置的连续空间,make_unique有个对应的重载版本,可以指定动态配置的长度:

#include <iostream>
#include<memory>
using namespace std;

int main() {
    auto arr = make_unique<int[]>(3);

    for(auto i = 0; i < 3; i++) {
        cout << arr[i] << endl;
    }

    return 0;
}

虽然可以如下动态配置连续空间,也可以自行指定删除器,然而意义不大就是了:

#include <iostream>
#include<memory>
using namespace std;

int main() {
    auto deleter = [](int *arr) { delete [] arr; };
    unique_ptr<int, decltype(deleter)> arr(new int[2] {0, 1}, deleter); 

    cout << *arr << endl;

    return 0;
}

在这个范例中,并不能对arr下标操作,也不能对arr进行加、减操作,因为并没有重载对应的运算符,这也说明了一件事,虽然许多文件会称unique_arr或之后要谈到的shared_ptr等为智慧指针(smart pointer),然而这并不正确,因为从这篇文件一开始,其实就知道,unique_arr等类型的实例并不是指针,它只是有指针部份行为罢了。

理解这个事实后,对于动态配置连续空间这件事,并想要以下标操作应该先前使用使用unique_ptrmake_unique的对应版本。

支持下标运算符版本的unique_ptr,也可以自定义删除器:

#include <iostream>
#include<memory>
using namespace std;

int main() {
    auto deleter = [](int arr[]) { delete [] arr; };
    unique_ptr<int[], decltype(deleter)> arr(new int[3] {1, 2, 3}, deleter);

    for(auto i = 0; i < 3; i++) {
        cout << arr[i] << endl;
    }

    return 0;
}

那么make_unique可否指定删除器呢?基本上make_unique是为了不需要自定义删除器的场合而存在的,因为指定了删除器,代表着你会使用delete,这就表示也必须对应的new存在,另外,由于支持下标操作的版本存在,自定义删除器的需求也减少了,若还是有需求,就直接在构造unique_ptr时指定。


展开阅读全文