自己實現 SharedPtr(2) —— 類型轉換、copy/move 語義的實現

由於今天還是繼續在做萬惡的(Android的)recovery升級,而每次編譯都要半個多小時,然後就抽空又來寫了一點點代碼。

在進入正題之前,先對之前的代碼做一點小小的改動:
1. PointerDeleter 類不再是模板而直接使用外圍類SharedPtr的模板參數
2. SharedPtr的模板參數從Reference改爲Resource

由於比較簡單,這個小改動就不上代碼了。



首先來說說轉型(cast)。我們希望SharedPtr能夠支持下面這樣的操作:

class Base {};
class Derived : public Base {};

SharedPtr<Base *> sp{new Derived{}};

就之前的實現而已,毫無疑問,這個操作是不允許的。因爲模板參數是Base *,而函數參數的類型是Derived *

我們這裏希望的是,他能夠像普通的指針一樣,比較,子類指針可以安全地賦值給父類指針。同時,對於這對於我們實現運行時多態(runtime polymorphism)也至關重要。

那麼,我們又當如何實現呢?

首先我們應該明確的一點是,對於用戶實際使用的任一參數化類型,我們都沒有辦法知道他到底有多少個子類,多什麼樣的子類!於是,爲了達到上面的目標,我們的構造函數需要能夠接受類型不確定完全參數。

講到這裏,答案其實已經很明確了,能夠接受任意類型作爲參數的,只能是函數模板了。

template<typename T>
SharedPtr(T t);

好吧,我說謊了,我們需要的不是任意類型,而是任意能夠進行安全轉換的類型。那麼,現在的問題就是,我們該如何得知,參數的實際類型是符合要求的呢?答案是,我不知道。事實上,作爲一個庫函數作者,我們也沒有辦法進行驗證。但是,事情也並非沒有任何轉機。雖然我們沒法驗證,但是編譯器可以幫我們驗證。

繼續上面的實例:

SharedPtr<Base *> sp{ptr};

這裏我們先不管如何驗證ptr指針有效性的問題。假定他是某個適當的類型,那麼,當ptrsp進行初始化後,在 SharedPtr<Base *>的內部,一般情況下,我們可以期望會有這麼一個語句:

Base* mPtr = ptr;

也許這只是一個很平凡的語句,但是,在平凡的背後,他卻揹負着類型安全的重任。C++作爲一門強類型,任何一個合格的編譯器,都會對這個初始化語句進行類型檢查

也許你已經猜到了,這就是讓智能指針進行類型轉換的關鍵。我們確實沒有辦法對類型進行檢查,但是,我們可以利用編譯器幫我們進行檢查。而對於我們,只需要假定該類型是合適的。

於是,之前的那個構造函數,現在可以成這樣:

template<typename Pointer>
SharedPtr(Pointer ptr)
    : SharedPtr{ptr, PointerDeleter{}} {
}

template<typename T, typename Deleter>
SharedPtr(T t, Deleter d) try
    : mSharedData{new SharedDataImpl<Deleter>{t, d}} {
} catch (std::bad_alloc) {
    d(t);
    throw;
}



接下來需要做的就是實現 copy/move 語言了。

由於我們把計數器、用戶的指針都存放到了內部的一個對象裏,所以,對於普通的 copy/move ctor (constructor) 的實現就很簡單了。只需要直接複製指針,然後修改計數器的值就可以了。

SharedPtr(const SharedPtr& sharedPtr)
    : mSharedData{sharedPtr.mSharedData} {
    ++mSharedData->refCount;
}

SharedPtr(SharedPtr&& sharedPtr)
    : mSharedData{sharedPtr.mSharedData} {
    sharedPtr.mSharedData = nullptr;
}


而對於copy/move 賦值運算符來說,就需要一點點小技巧了。由於賦值和構造函數的直接主體的邏輯是一致的,我們並不想在兩個函數之間重複代碼(Don’t repeat yourself)。

爲了不重複代碼,可以使用C++的所謂 copy-swap idiom。而對於C++11,就是 copy/move-swap了。

首先,我們需要定義一個成員函數swap(當然,用獨立的函數也可以,只是需要聲明他是你的friend):

void swap(SharedPtr& other) {
    std::swap(mSharedData, other.mSharedData);
}

然後 ,我們的 operator=()就可以這樣定義:

SharedPtr& operator=(SharedPtr sp) {
    swap(sp);
    return *this;
}

注意這裏的參數類型不是const SharedPtr&,也不是SharedPtr&&,而是 SharedPtr。如此一來,根據函數的實參,sp初始化時會自動調用 move constructor 或 copy constructor。當函數返回時,swapsp裏的數據會自動 destruct,我們並不需要顯式釋放資源。同時,self-assign 問題也解決了。


下面是到目前爲止的代碼:

#include <new>

#ifndef LIBS_UTIL_SHARED_PTR_H_
#define LIBS_UTIL_SHARED_PTR_H_


#define DEBUG_SHARED_PTR

#if defined(DEBUG_SHARED_PTR)
#include <iostream>
#endif


namespace jl_util {


template<typename Resource>
class SharedPtr {
public:
    template<typename Pointer>
    SharedPtr(Pointer ptr)
        : SharedPtr{ptr, PointerDeleter{}} {
    }

    template<typename T, typename Deleter>
    SharedPtr(T t, Deleter d) try
        : mSharedData{new SharedDataImpl<Deleter>{t, d}} {
    } catch (std::bad_alloc) {
        d(t);
        throw;
    }

    SharedPtr(const SharedPtr& sharedPtr)
        : mSharedData{sharedPtr.mSharedData} {
        ++mSharedData->refCount;
    }

    SharedPtr(SharedPtr&& sharedPtr)
        : mSharedData{sharedPtr.mSharedData} {
        sharedPtr.mSharedData = nullptr;
    }

    SharedPtr& operator=(SharedPtr sharedPtr) {
        swap(sharedPtr);
        return *this;
    }

    ~SharedPtr() {
        if (!mSharedData) return;
        if (--mSharedData->refCount == 0) {
            delete mSharedData;
#if defined(DEBUG_SHARED_PTR)
            std::cout << "~SharedPtr(): resource destroyed"
                      << std::endl;
        }
#endif
    }


private:
    class PointerDeleter {
    public:
        void operator()(Resource ref) {
            delete ref;
        }
    };

    class SharedData {
    public:
        int refCount = 1;
        Resource resource;

        virtual ~SharedData() {
        }

    protected:
        SharedData(Resource r) : resource{r} {
        }
    };

    template<typename Deleter>
    class SharedDataImpl: public SharedData {
    public:
        Deleter deleter;

        SharedDataImpl(Resource r, Deleter d)
            : SharedData{r}, deleter{d} {
            }

        ~SharedDataImpl() {
            deleter(SharedData::resource);
        }
    };

    SharedData* mSharedData;

    void swap(SharedPtr& other) {
        std::swap(mSharedData, other.mSharedData);
    }
};


}

#endif




到了這裏,我想爲文章的讀者道個歉,因爲在寫的過程中,我才發現了,目前的實現對應類型轉換的支持並不完備。而且,由於目前的實現不夠靈活,導致無法輕鬆添加這個功能。不管怎麼樣,這個還是留待下一篇文章再加以完善了。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章