由於今天還是繼續在做萬惡的(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
指針有效性的問題。假定他是某個適當的類型,那麼,當ptr
對sp
進行初始化後,在 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。當函數返回時,swap
到sp
裏的數據會自動 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
到了這裏,我想爲文章的讀者道個歉,因爲在寫的過程中,我才發現了,目前的實現對應類型轉換的支持並不完備。而且,由於目前的實現不夠靈活,導致無法輕鬆添加這個功能。不管怎麼樣,這個還是留待下一篇文章再加以完善了。