如何实现可存储可变参数C++ Lambda的connect与invoke机制?
实现信号-回调架构的完整设计方案
这个需求很典型,要实现一套支持普通函数、Lambda,还能处理参数调用的信号架构,我们可以分核心模块来拆解设计,下面一步步来:
1. 回调存储的多态结构
首先得解决「存储不同签名可调用对象」的问题——普通函数、Lambda的参数列表可能完全不一样,这里用抽象基类+模板派生类的多态方案来统一管理:
#include <memory> #include <vector> #include <unordered_map> #include <functional> #include <utility> #include <algorithm> // 回调抽象基类,提供统一的销毁和连接ID管理接口 class CallbackBase { public: virtual ~CallbackBase() = default; int get_connection_id() const { return connection_id_; } void set_connection_id(int id) { connection_id_ = id; } private: int connection_id_ = -1; // 用于后续断开连接 }; // 模板派生类,适配具体签名的可调用对象 template<typename... Args> class Callback : public CallbackBase { public: using FuncType = std::function<void(Args...)>; explicit Callback(FuncType func) : func_(std::move(func)) {} // 执行回调,完美转发参数 void execute(Args... args) const { func_(std::forward<Args>(args)...); } private: FuncType func_; };
2. 信号管理器:维护信号与回调的映射
我们需要一个单例的信号管理器,负责存储「信号ID → 回调列表」的映射,同时处理连接、触发、断开的逻辑:
class SignalManager { public: // 单例模式,确保全局唯一实例 static SignalManager& instance() { static SignalManager manager; return manager; } // 重载1:连接普通函数指针 template<typename R, typename... Args> int connect(int signal_id, R(*func)(Args...)) { auto callback = std::make_unique<Callback<Args...>>(wrap_function(func)); return add_callback(signal_id, std::move(callback)); } // 重载2:连接Lambda/其他可调用对象(比如std::bind的结果) template<typename Callable> int connect(int signal_id, Callable&& callable) { // 推导Lambda的operator()签名,调用辅助函数 using Operator = decltype(&Callable::operator()); return connect_impl(std::forward<Callable>(callable), signal_id, Operator{}); } // 触发信号:遍历对应回调,调用匹配签名的实例 template<typename... Args> void invoke(int signal_id, Args&&... args) { auto it = signal_map_.find(signal_id); if (it == signal_map_.end()) return; for (const auto& cb_ptr : it->second) { // 尝试将基类指针转为对应签名的派生类指针 auto* cb = dynamic_cast<Callback<Args...>*>(cb_ptr.get()); if (cb) { cb->execute(std::forward<Args>(args)...); } // 不匹配的回调会被忽略,你也可以添加日志或抛出异常提示 } } // 断开指定连接 void disconnect(int signal_id, int connection_id) { auto it = signal_map_.find(signal_id); if (it == signal_map_.end()) return; auto& callbacks = it->second; callbacks.erase( std::remove_if(callbacks.begin(), callbacks.end(), [connection_id](const std::unique_ptr<CallbackBase>& cb) { return cb->get_connection_id() == connection_id; }), callbacks.end() ); } private: SignalManager() = default; SignalManager(const SignalManager&) = delete; SignalManager& operator=(const SignalManager&) = delete; // 辅助函数:把普通函数转为std::function template<typename R, typename... Args> static std::function<void(Args...)> wrap_function(R(*func)(Args...)) { return [func](Args... args) { func(std::forward<Args>(args)...); }; } // 辅助推导Lambda的operator()签名 template<typename Lambda, typename R, typename... Args> int connect_impl(Lambda&& lambda, int signal_id, R(Lambda::*)(Args...) const) { auto callback = std::make_unique<Callback<Args...>>( std::function<void(Args...)>(std::forward<Lambda>(lambda)) ); return add_callback(signal_id, std::move(callback)); } // 添加回调到列表,并生成唯一连接ID int add_callback(int signal_id, std::unique_ptr<CallbackBase> callback) { static int next_conn_id = 0; const int conn_id = next_conn_id++; callback->set_connection_id(conn_id); signal_map_[signal_id].push_back(std::move(callback)); return conn_id; } // 核心存储:信号ID对应回调列表 std::unordered_map<int, std::vector<std::unique_ptr<CallbackBase>>> signal_map_; };
3. 全局便捷接口
为了让用户使用更顺手,封装全局的connect和invoke函数,不需要直接操作SignalManager:
template<typename Callable> int connect(int signal_id, Callable&& callable) { return SignalManager::instance().connect(signal_id, std::forward<Callable>(callable)); } template<typename... Args> void invoke(int signal_id, Args&&... args) { SignalManager::instance().invoke(signal_id, std::forward<Args>(args)...); }
4. 测试你的示例代码
现在可以直接运行你给出的测试场景,包括处理默认参数的情况:
#include <iostream> void some_func1() { std::cout << "Executing some_func1\n"; } void some_func2(int a, float b = 3.14f) { std::cout << "Executing some_func2: a=" << a << ", b=" << b << "\n"; } enum { SIGNAL0, SIGNAL1 }; int main() { // 连接各种回调 int con0 = connect(SIGNAL0, some_func1); int con1 = connect(SIGNAL1, some_func2); int con2 = connect(SIGNAL0, []{ std::cout << "Executing SIGNAL0 Lambda\n"; }); int con3 = connect(SIGNAL1, [](int a, float b){ std::cout << "Executing SIGNAL1 Lambda: a=" << a << ", b=" << b << "\n"; }); // 触发信号 invoke(SIGNAL0); invoke(SIGNAL1, 123, 3.14f); // 处理默认参数的场景 // 注意:C++运行期无法获取函数默认参数,必须手动绑定 int con4 = connect(SIGNAL1, [](int a){ some_func2(a); }); // Lambda包装 int con5 = connect(SIGNAL1, std::bind(some_func2, std::placeholders::_1, 3.14f)); // std::bind绑定 invoke(SIGNAL1, 123); // 此时会调用con4、con5对应的回调 return 0; }
关于默认参数的说明
你提到的invoke(SIGNAL1, 123)自动使用默认参数的需求,在C++中无法直接实现,原因是:
- 函数的默认参数是编译期特性,编译器不会把默认参数值保留到运行期,所以架构无法在invoke时自动补全。
- 即使C++14及以后Lambda支持默认参数,它的签名依然是完整的(比如
(int, float)),而invoke(SIGNAL1,123)传递的是单个int参数,类型不匹配,无法触发该Lambda。
解决办法就是像上面示例那样,在connect时手动把默认参数绑定到回调中,生成一个适配少参数的包装器。
内容的提问来源于stack exchange,提问作者Alex Wennström




