You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何实现可存储可变参数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. 全局便捷接口

为了让用户使用更顺手,封装全局的connectinvoke函数,不需要直接操作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

火山引擎 最新活动