首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> VC 教學

C++ 委托、信號與槽機制模板實現:從 MFC 消息映射到類型安全回調

     編輯:VC 教學

C++ 標準庫無內建信號/槽。MFC 用宏實現消息映射,Qt 用 MOC 預編譯,Boost.Signals2/ libsigc++ 提供庫級實現。本文探討**純模板、無外部依賴、類型安全**的委托/信號實現思路:成員函數指針綁定、可變參數模板、連接管理、生命週期安全、線程安全擴展,適合嵌入式/輕量級項目集成。

核心抽象:Delegate (委托/可調用對象包裝器)


// 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/SlotsMOC 編譯期生成、字符串簽名、動態屬性、QObject 樹管理Qt 框架Qt 項目首選
本文模板版零依賴、頭文件唯一、編譯期類型安全、極簡無 (C++17)嵌入式、輕量級、教學、禁止引入大型庫

避坑指南

  1. ❌ **Lambda 捕獲引用懸空** — 連接時捕獲 `[&]` 並在發射前銷毀 → 改用 `[=]` 或共享所有權 ``;
  2. ❌ **槽內拋異常導致後續槽不執行** — `Emit` 內捕獲並記錄,或用 `try-catch` 包裝每個槽;
  3. ❌ **遞歸發射死循環** — 槽內再次 `Emit` 同信號 → 設置遞歸深度計數或標志位防重入;
  4. ✅ **連接返回 RAII 對象** — `auto conn = signal.Connect(...); // 離開作用域自動斷開`;
  5. ✅ **靜態分析友好** — 模板版編譯期檢查簽名匹配,優於字符串基 Qt/MFC。

信號/槽是「好萊塢原則」 (Don't call us, we'll call you) 的 C++ 實踐。理解其模板實現本質 — **類型擦除 + 可變參數完美轉發 + 生命週期協議** — 能在無法引入大型框架時快速構建解耦事件系統。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved