shared_ptr


很多情況下,動態配置的物件會在不同的類別實例間共享,很自然地就會引發一個問題,誰該負責刪除這個被分享的、動態配置的物件?

答案可以很簡單,最後一個持有動態配置物件的實例不再需要該物件時,該實例要負責刪除物件,想採用這個答案,要解決的就是,怎麼知道誰是最後一個持有物件的實例?

如果有個 SharedPtr 可以管理動態配置物件,SharedPtr 實例共用一個計數器,它記錄有幾個 SharedPtr 實例共享該物件,每多一個 SharedPtr 實例共享物件時,計數器增一,若共享物件的 SharedPtr 實例被摧毀時,計數器減一,若有個 SharedPtr 實例發現計數器為零時,就將共享的物件刪除。

當然,想實作這樣的 SharedPtr 也是點挑戰性,不過若能實現,對 C++ 11 以後標準程式庫提供的 shared_ptr 就會更能掌握,就來實現個簡單版本吧!

#include <iostream>
using namespace std;

template<typename T>
class SharedPtr {
    using Deleter = void (*)(T*);

    T* p = nullptr;
    size_t* pctr = nullptr; // 參考計數
    Deleter del = nullptr;

    // 被交換的 sharedPtr,參考計數是否減一
    // 就看還有沒有在其他處被引用
    void swap(SharedPtr& sharedPtr) {
        using std::swap;
        swap(this->p, sharedPtr.p);
        swap(this->pctr, sharedPtr.pctr);
        swap(this->del, sharedPtr.del);
    }    

public:
    SharedPtr(T* p = nullptr, Deleter del = nullptr) : 
        p(p), pctr(new size_t(p != nullptr)), del(del) {}

    SharedPtr(const SharedPtr& sharedPtr) : p(sharedPtr.p), pctr(sharedPtr.pctr), del(sharedPtr.del) {
        // 參考計數加一
        ++*(this->pctr);
    }

    SharedPtr(SharedPtr&& sharedPtr) : SharedPtr() { 
        this->swap(sharedPtr); 
    }

    // sharedPtr 參數在執行過後就摧毀了,參考計數會減一
    SharedPtr& operator=(SharedPtr sharedPtr) {
        this->swap(sharedPtr);
        return *this;
    }

    ~SharedPtr() {
        if(this->p == nullptr) {
            return;
        }

        // 參考計數減一
        if(--*(this->pctr) == 0) {
            // 若參考計數為零,刪除資源
            this->del ? this->del(this->p) : delete this->p;
            delete pctr;
        }
    }

    void reset(T *p = nullptr, Deleter del = nullptr) {
        // wrapper 參數在執行過後就摧毀了,參考計數會減一
        SharedPtr wrapper(p, del);
        this->swap(wrapper);
    }    

    // 令 SharedPtr 行為像個指標
    T& operator*() { return *p; }
    T* operator->() { return p; }
};

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

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

    f2 = f1;
    f3 = SharedPtr<Foo>(new Foo(30));

    SharedPtr<int> arr(new int[3] {1, 2, 3}, [](int *arr) { delete [] arr; });

    return 0;
}

這個簡單版本也考慮了自訂刪除器的指定,你可能會發現,怎麼與 unique_ptr 不太一樣,這是因為 shared_ptr 的刪除器是共享的,不若 unique_ptr 是各自管理著一個資源,而有各自的刪除器,在實作上,必須得在執行時期判斷是否有指定刪除器,決定要使用刪除器,還是 delete

C++ 11 提供了 shared_ptr,定義在 memory 標頭,上面的範例基本上就是模仿了 shared_ptr,來看看 shared_ptr 的使用:

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

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

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

    f2 = f1;
    f3 = shared_ptr<Foo>(new Foo(30));

    shared_ptr<int> arr(new int[3] {1, 2, 3}, [](int *arr) { delete [] arr; });

    return 0;
}

雖然可以直接建構 shared_ptr 實例,然而在不指定刪除器的情況下,建議透過 make_shared,可以避免使用 new

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

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

int main() {
    auto f1 = make_shared<Foo>(10); 
    auto f2 = make_shared<Foo>(20); 
    auto f3(f1);

    f2 = f1;
    f3 = make_shared<Foo>(30); 

    return 0;
}

shared_ptr 實例可以透過 unique 方法,得知動態配置的物件是否與其他 shared_ptr 實例共享,透過 use_count 方法可以取得參考計數,shared_ptr 沒有像 unique_ptr 提供有可使用下標運算子的版本,本身也不支援加、減運算,因此對於動態配置的連續空間,若要取得指定空間的值,必須透過 get 取得管理的資源。例如:

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

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

    for(int *p = arr.get(), i = 0; i < 3; i++) {
        cout << *(p + i) << endl;
    }

    return 0;
}