上篇 (事件程式設計 (一)) 實作 CPrimeCalculator 觸發 Progress/Done 事件,客戶端實作 IPrimeEvents 介面並註冊。但事件觸發程式碼冗長重複:每個事件需寫 NotifyFoo 遍歷客戶列表呼叫 OnFoo。且事件介面與事件源分離,難以重用。
將事件機制封裝為單一可重用類別,使用仿函數 與模板 消除樣板程式碼。
仿函數將函數轉為類別,取代回呼函數指標。配合 STL for_each 演算法:
class NotifyProgress : public unary_function<IPrimeEvents*, void>
{
UINT m_nFound;
public:
NotifyProgress(UINT n) : m_nFound(n) {}
void operator()(IPrimeEvents* obj) { obj->OnProgress(m_nFound); }
};
繼承 unary_function 使其可配接 STL 配接器 (如 not1, bind2nd)。
class IPrimeEvents
{
public:
virtual ~IPrimeEvents() = 0;
virtual void OnProgress(UINT nFound) {}
virtual void OnDone() {}
// 內嵌仿函數類別
class Progress : public unary_function<IPrimeEvents*, void>
{
UINT m_nFound;
public:
Progress(UINT n) : m_nFound(n) {}
void operator()(IPrimeEvents* obj) { obj->OnProgress(m_nFound); }
};
class Done : public unary_function<IPrimeEvents*, void>
{
public:
void operator()(IPrimeEvents* obj) { obj->OnDone(); }
};
};
inline IPrimeEvents::~IPrimeEvents() {}
template <class I>
class CEventMgr
{
list<I*> m_clients;
public:
void Register(I* p) { m_clients.push_back(p); }
void Unregister(I* p) { m_clients.remove(p); }
template <class F>
void Raise(F fn)
{
for_each(m_clients.begin(), m_clients.end(), fn);
}
};
觸發事件僅需一行:
m_eventmgr.Raise(IPrimeEvents::Progress(nFound));
編譯器自動具現化 Raise<IPrimeEvents::Progress>,內部以 for_each 呼叫每個客戶的 operator(),最終觸發 OnProgress。
class IPrimeEvents
{
DECLARE_EVENTS(IPrimeEvents);
public:
DEFINE_EVENT1(IPrimeEvents, Progress, UINT);
DEFINE_EVENT0(IPrimeEvents, Done);
};
IMPLEMENT_EVENTS(IPrimeEvents);
DEFINE_EVENT0/1/2 分別對應 0/1/2 參數事件,自動產生處理函數與仿函數。超過 2 參數建議封裝結構體傳指標。
仿函數通常比函數指標更有效率:模板具現化時編譯器內聯展開,僅剩虛擬函數呼叫開銷。傳函數指標無法內聯。