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 時指定。