在稍大點的軟體工程中,我們經常需要用到基於介面的回調在達到模組解耦/通訊的目的.甚至在超大的工程中,類似這樣的回調機制比如委託(Delegate)會成為系統重要的基礎組件,為分模組的系統開發提供良好的底層支援.
但是隨著這樣的機制被大量使用,指標的生命週期管理,迴圈重入等問題導致野指標的問題就越來越突出,極大的破壞了軟體的穩定性,造成業務的損失.本文重點探討提供一種機制,把這種情況下野指標的危險降到最低.
首先我們看一種典型的應用情境.socket模組通常需要通知外部模組一些資訊,比如收包,連錯錯誤/成功之類的.一般會有如下調用;
pISocket->setSocketSink(pSocketSink);
這裡設定了回調後,如果指標的生命週期管理失當,就會形成野指標而宕機.實際應用中可能情況更複雜.比如典型的 mmorpg中,模組能達到幾十個.他們的互動(更多的是基於委託)會更加頻繁和複雜,出問題的可能性會大很多.
為了能夠讓所有有儲存指標的模組能感知到指標的失效,需要把回調指標再包一層.這個額外的中介層,主要包含以下三個參數:
template<class clsSink>
class SafeHandle
{
clsSink* m_pSink;
//回調指標.
int* m_refCount;
//引用計數,控制所有引用同一個指標的控制代碼相關動態申請變數的生存周期
bool* m_isValid;
//一旦無效,可以讓所有引用該指標的控制代碼都無效..
}
再讓回調介面繼承以下介面:
template<class clsSink>
class ISafeSink
{
public:
typedef SafeHandle<clsSink> SinkSafeCls;
private:
SinkSafeCls m_safeHandle;
public:
ISafeSink()
{
m_safeHandle.setNewSink(static_cast<clsSink*>(this));
}
~ISafeSink(){ m_safeHandle.setValid(false);}
const SinkSafeCls& getSafehandle() { return m_safeHandle;}
};
對於回調指標而言,如下繼承即可:
class ISimple : public ISafeSink<ISimple>
{
};
這裡雖然解決了野指標問題,但是對於安全回調和非安全的,使用這套東西的人卻需要不同的編碼.即 前者儲存的是SafeHandle<clsSink>,後者直接儲存回調指標.為了保持一致性,讓外部使用者不用關心回調的實現細節.我們需要做更深一層的封裝.
我們首先需要做的根據回調介面是否有繼承自ISafeSink,定義/擷取不同的類型。再把
SafeHandle操作符重載.讓SafeHandle<clsSink>跟單獨的回調指標具有相同的外部調用邏輯.其中 判斷是否有繼承關係的類大概如下:
template<class Base,class Derived>
class IsDerived
{
struct char2 { char c[2];};
static Derived& makeDerived();
static char test(...);
static char2 test(const Base&);
typedef SaveType<sizeof(test(makeDerived()))==sizeof(char) ? 1 : 0, Derived> HelperType;
};
我們使用這個回調實現的話,如下調用即可
class testClass
{
SavePtrThis<ISimple>::PtrType m_safeSink;
public:
void addSink(ISimple * p)
{
m_safeSink = SavePtrThis<ISimple>::FuncType::get_ptr(p);
}
void run()
{
if(!m_safeSink)
{
printf("控制代碼非法..\n");
return;
}
m_safeSink->testFunc();
}
};
最後附上單元測試的代碼,在VC9及gcc 4.4編譯通過
http://kevincodelib.googlecode.com/files/SafeSink.zip