承接上篇,本文深入探討訊號槽機制的進階特性:多對多連接實作、函數物件統一介面、類別爆炸問題的解決方案,以及以資料驅動的動態組裝架構。
一個訊號可連接多個槽,一個槽可監聽多個訊號。最簡單實作:在發送者內部維護槽指標容器(std::vector<Slot*>)。
class MultiSlot : public AbstractSlot {
std::vector slots_;
public:
void connect(AbstractSlot* s) { slots_.push_back(s); }
void operator()(Args... args) override {
for (auto* s : slots_) (*s)(args...); // 呼叫所有槽
}
};
使用 boost::shared_ptr 管理槽生命週期,避免記憶體洩漏與懸空指標。
傳統回調物件模式需為每個動作定義衍生類(PageReloadAction, PageLoadAction...),導致類別數量失控。解決方案:泛型函數包裝器。
std::function / Boost.Function// 定義槽型別:可儲存任何可呼叫物件(函數、Lambda、bind 結果、函數物件)
using SlotFunc = std::function;
class Button {
std::vector slots_;
public:
void connect(SlotFunc f) { slots_.push_back(f); }
void clicked(int x, const std::string& msg) {
for (auto& f : slots_) f(x, msg);
}
};
// 使用範例
Button btn;
btn.connect([](int x, const std::string& msg) { std::cout << "Lambda: " << msg << std::endl; });
btn.connect(std::bind(&Page::reload, &page, std::placeholders::_1));
btn.connect([](int, const std::string&) { /* 另一個動作 */ });
優勢:零額外類別定義,支援 Lambda、成員函數綁定、函數指標,極大簡化客戶端程式碼。
operator() 的威力讓函數物件表現如函數:
struct Action {
void operator()(int x) { /* ... */ }
};
Action act;
act(42); // 像呼叫函數一樣
std::for_each(v.begin(), v.end(), Action()); // 直接用於 STL 演算法
這是泛型程式設計(模板、std::function, bind)的基石。
| 維度 | 程式碼連接 | 資料驅動連接 |
|---|---|---|
| 型別安全 | 編譯期保證 | 執行期檢查(或放棄) |
| 動態性 | 靜態 | 執行期可修改/載入 |
| 適用場景 | 應用程式邏輯 | UI 設計器、外掛系統、腳本整合 |
| 實作複雜度 | 低 | 高(需註冊表、工廠、序列化) |
std::map, std::map{"signal": "button.clicked", "slot": "page.reload"} (JSON/XML)Variant/Any,槽端解包(犧牲型別安全)。Qt 透過 Meta-Object Compiler (MOC) 在編譯期擴充 C++,自動產生:
QMetaObject::activate)。QObject::connect(sender, SIGNAL(name()), receiver, SLOT(name())) 字串連接。這是「編譯期型別安全」與「執行期動態連接」的獨特平衡。
std::function 版。訊號槽機制從「解決耦合」演進為「支援動態組裝」。現代 C++ 以 std::function + Lambda 完美解決了「類別爆炸」與「型別安全」的矛盾。架構師應根據專案動態性需求,在輕量級庫與重量級框架間做出理性選擇:不需要動態組裝時,請堅持輕量級方案。