在稍大点的软件工程中,我们经常需要用到基于接口的回调在达到模块解耦/通信的目的.甚至在超大的工程中,类似这样的回调机制比如委托(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