在 .NET 框架中,委派是實現事件驅動程式設計的核心機制。它允許類別註冊事件處理器,當事件觸發時自動呼叫已註冊的方法。然而在標準 C++ 中,特別是處理非靜態成員函式作為回呼函式時,卻面臨著類型安全與物件上下文綁定的雙重挑戰。本文將詳細介紹一種完整的解決方案,透過函數物件與巨集模板技術,實現支援靜態成員函式、非靜態成員函式、全域函式的類型安全委派系統。
根據 .NET 定義,委派是一個可持有方法參考的類別,擁有固定的簽章(回傳型別、參數型別與數量),只能引用符合該簽章的方法。本質上,它是一個類型安全的函式指標。提供委派的類別允許外部在委派上註冊處理函式;當委派被呼叫時,會依序執行所有已註冊的處理函式,而來源類別無需知悉具體註冊了多少處理器——委派內部自行管理列表與呼叫邏輯。
C++ 中實現委派的關鍵在於函數物件。透過重載 operator(),使物件可像函式一樣被呼叫,同時內部封裝物件指標與成員函式指標,解決非靜態成員函式需綁定特定物件實例的問題。
template <class T>
class Functor {
public:
Functor(T* pObj, int (T::*pFunc)(int)) {
m_pObject = pObj;
m_pFunction = pFunc;
}
int operator()(int p) {
return (m_pObject->*m_pFunction)(p);
}
private:
T* m_pObject;
int (T::*m_pFunction)(int);
};
使用方式:
class MyClass {
public:
int Square(int p) { return p * p; }
};
void some_function() {
MyClass theClass;
Functor<MyClass> myFunc(&theClass, &MyClass::Square);
int result = myFunc(5); // result = 25
}
為了統一管理不同類別的函數物件,引入抽象基類 Functor 與模板衍生類 TemplateFunctor<T>:
class Functor {
public:
virtual int operator()(int) = 0;
};
template <class T>
class TemplateFunctor : public Functor {
public:
TemplateFunctor(T* pObj, int (T::*pFunc)(int)) {
m_pObject = pObj;
m_pFunction = pFunc;
}
int operator()(int p) override {
return (m_pObject->*m_pFunction)(p);
}
private:
T* m_pObject;
int (T::*m_pFunction)(int);
};
此設計允許以基類指標統一操作各種具體函數物件,實現執行期多型。
針對不同參數列表與回傳型別,手動撰寫函數物件類別極為繁瑣。利用巨集 DECLARE_FUNCTOR 可自動產生對應的基類與模板類:
#define DECLARE_FUNCTOR(name, parmdecl, parmcall)
class name##Functor {
public:
virtual void operator() parmdecl = 0;
};
template <class C>
class name##TFunctor : public name##Functor {
public:
name##TFunctor(C* pObj, void (C::*pFunc)parmdecl) {
m_pObj = pObj; m_pFunc = pFunc;
}
void operator() parmdecl { (m_pObj->*m_pFunc)parmcall; }
private:
C* m_pObj;
void (C::*m_pFunc)parmdecl;
};
參數說明:
name:函數物件名稱前綴parmdecl:運算子參數宣告(含括號),如 (int p1, int p2)parmcall:實際呼叫參數列表,如 (p1, p2)範例:DECLARE_FUNCTOR(Add, (int p1, int p2), (p1, p2)) 將展開為處理兩個 int 參數的 AddFunctor 與 AddTFunctor。
委派在函數物件基礎上進一步封裝,維護一個函數物件指標的動態陣列(std::vector<Functor*>),支援多重回呼:
#define DECLARE_DELEGATE(name, parmdecl, parmcall)
namespace name##Delegate {
class Delegate {
public:
Delegate(); ~Delegate();
template <class C> void Add(C* pObj, void (C::*pFunc)parmdecl);
void Add(void (*pFunc)parmdecl); // 靜態/全域函式
template <class C> void Remove(C* pObj, void (C::*pFunc)parmdecl);
void Remove(void (*pFunc)parmdecl);
void operator+=(Functor* pFunc);
void operator+=(void (*pFunc)parmdecl);
void operator-=(Functor* pFunc);
void operator-=(void (*pFunc)parmdecl);
void Invoke parmdecl;
void operator() parmdecl { Invoke parmcall; }
private:
std::vector<Functor*> m_pFuncs;
typedef std::vector<Functor*>::iterator vit;
};
}
template <class C>
name##Delegate::TFunctor<C>* name##Handler(C* pObj, void (C::*pFunc)parmdecl) {
return new name##Delegate::TFunctor<C>(pObj, pFunc);
}
vector 儲存基類指標,可容納任意具體類型的函數物件。Add/Remove 與 +=/-= 兩組等效操作,並支援成員函式與非成員函式重載。FFunctor 封裝普通函式指標,無需物件指標。DECLARE_DELEGATE(Add, (int p1, float p2), (p1, p2))
class A {
public:
A() : value(5) {}
virtual void Fun1(int val, float val2) {
value = val * 2 * (int)val2;
cout << "[A::Fun1] " << val << ", " << val2 << endl;
}
static void StaticFunc(int val, float val2) {
cout << "[A::StaticFunc] " << val << ", " << val2 << endl;
}
int value;
};
class B : public A {
public:
void Fun1(int val, float val2) override {
value += val * 3 * (int)val2;
cout << "[B::Fun1] " << val << ", " << val2 << endl;
}
};
void GlobalFunc(int val, float val2) {
cout << "[GlobalFunc] " << val << ", " << val2 << endl;
}
int main() {
A a; B b;
AddDelegate::Delegate del;
// 註冊處理器
del += AddHandler(&a, &A::Fun1);
del += AddHandler(&b, &B::Fun1);
del += GlobalFunc;
del += A::StaticFunc;
// 觸發委派
del(4, 5); // 依序呼叫四個處理器
// 移除部分處理器後再次觸發
del -= AddHandler(&a, &A::Fun1);
del -= A::StaticFunc;
del(4, 5);
return 0;
}
完整的 delegate.h 包含上述所有巨集與類別定義,並處理了 STL 警告抑制、記憶體管理、比較運算子等細節。核心邏輯包含:
Functor 抽象基類定義 Invoke、isMethod、operator==TFunctor<C> 實作成員函式綁定與比較FFunctor 實作靜態/全域函式封裝Delegate 管理 vector<Functor*>,提供增刪查與呼叫name##Handler 簡化函數物件建立此實現展示了如何在 C++ 中以模板、巨集與多型技術,構建出類似 .NET 的類型安全委派系統。雖然現代 C++ (C++11 起) 提供了 std::function、std::bind、Lambda 表達式等更強大的標準工具,但理解這套經典實作的原理,對於掌握 C++ 物件模型、函式指標與模板元程式設計仍具備重要教學價值。未來可延伸方向包括:支援非 void 回傳值並收集結果、固定參數個數的特化版本、以及與現代 C++ 標準庫的互操作。