C++ 標準庫無內建信號/槽。MFC 用宏實現消息映射,Qt 用 MOC 預編譯,Boost.Signals2/ libsigc++ 提供庫級實現。本文探討**純模板、無外部依賴、類型安全**的委托/信號實現思路:成員函數指針綁定、可變參數模板、連接管理、生命週期安全、線程安全擴展,適合嵌入式/輕量級項目集成。
// C++17 可變參數模板版
template
class Delegate; // 主模板未定義
template
class Delegate {
using FuncPtr = Ret(*)(void*, Args...);
void* obj_ = nullptr; // 對象指針 (自由函數為 nullptr)
FuncPtr func_ = nullptr; // 統一調用樁
// 靜態樁函數:成員函數調用適配器
template
static Ret MemberStub(void* obj, Args... args) {
return (static_cast(obj)->*MemFn)(std::forward(args)...);
}
template
static Ret ConstMemberStub(void* obj, Args... args) {
return (static_cast(obj)->*MemFn)(std::forward(args)...);
}
template
static Ret FreeStub(void*, Args... args) {
return FreeFn(std::forward(args)...);
}
template
static Ret LambdaStub(void* obj, Args... args) {
return (*static_cast(obj))(std::forward(args)...);
}
public:
Delegate() = default;
Delegate(nullptr_t) {}
// 綁定成員函數
template
void Bind(T* obj) {
obj_ = obj;
func_ = &MemberStub;
}
template
void BindConst(T* obj) {
obj_ = obj;
func_ = &ConstMemberStub;
}
// 綁定自由函數
template
void Bind() {
obj_ = nullptr;
func_ = &FreeStub;
}
// 綁定 Lambda/可調用對象 (需生命週期管理)
template
void Bind(F* callable) {
obj_ = callable;
func_ = &LambdaStub;
}
// 調用
Ret operator()(Args... args) const {
if (!func_) throw bad_function_call();
return func_(obj_, std::forward(args)...);
}
explicit operator bool() const { return func_; }
};
template
class Signal; // 主模板
template
class Signal {
using DelegateType = Delegate;
vector slots_;
public:
// 連接:返回連接 ID (用於斷開)
size_t Connect(const DelegateType& d) {
slots_.push_back(d);
return slots_.size() - 1;
}
template
size_t Connect(T* obj) {
DelegateType d; d.Bind(obj);
return Connect(d);
}
// 斷開
void Disconnect(size_t id) {
if (id < slots_.size()) slots_[id] = DelegateType(); // 清空
}
void DisconnectAll() { slots_.clear(); }
// 發射 (廣播)
void Emit(Args... args) {
for (auto& slot : slots_) {
if (slot) slot(std::forward(args)...);
}
}
// 發射並收集返回值 (僅當 Ret 非 void)
vector EmitCollect(Args... args) {
vector results;
for (auto& slot : slots_) if (slot) results.push_back(slot(std::forward(args)...));
return results;
}
};
class Button {
public:
Signal OnClick; // 無參無返回信號
void Click() { OnClick.Emit(); }
};
class Window {
int count_ = 0;
public:
void OnButtonClick() { cout << "Clicked " << ++count_ << " times\n"; }
void OnButtonClickConst() const { cout << "Const clicked\n"; }
};
int main() {
Button btn;
Window win;
// 連接成員函數
btn.OnClick.Connect(&win);
// 連接 Lambda (需保證生命週期)
auto lambda = []{ cout << "Lambda triggered\n"; };
btn.OnClick.Connect(&lambda);
btn.Click(); // 輸出兩行
btn.Click();
return 0;
}
| 需求 | 實現方向 |
|---|---|
| 自動斷開 (對象銷毀時) | 引入 Connection RAII 對象,析構時自動 Disconnect;或用 weak_ptr 監控對象存活 |
| 線程安全發射 | Emit 中加鎖 (`shared_mutex`),或複製槽列表後釋放鎖再調用 (避免死鎖) |
| 異步發射 | 投遞到線程池/事件循環 (post/dispatch) |
| 參數傳值/引用/移動語義 | 模板完美轉發 `Args&&...`,注意 Lambda 捕獲引用懸空 |
| 優先級/順序控制 | 槽存儲 `pair |
| 類型擦除統一存儲 (異構信號) | `std::function` / `any` / 自製 type-erased wrapper |
| 庫 | 特點 | 依賴 | 適用場景 |
|---|---|---|---|
| Boost.Signals2 | 線程安全、自動連接管理、組合槽、豐富組合器 | Boost (頭文件) | 大型項目、需健壯生命週期管理 |
| libsigc++ | GTKmm 依賴、純 C++、語法簡潔 | 獨立庫 | 跨平台 GUI/邏輯分離 |
| Qt Signals/Slots | MOC 編譯期生成、字符串簽名、動態屬性、QObject 樹管理 | Qt 框架 | Qt 項目首選 |
| 本文模板版 | 零依賴、頭文件唯一、編譯期類型安全、極簡 | 無 (C++17) | 嵌入式、輕量級、教學、禁止引入大型庫 |
信號/槽是「好萊塢原則」 (Don't call us, we'll call you) 的 C++ 實踐。理解其模板實現本質 — **類型擦除 + 可變參數完美轉發 + 生命週期協議** — 能在無法引入大型框架時快速構建解耦事件系統。