You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让std::function<void(const Event&)>接受参数为子类的Lambda?

实现自动向下转型的事件监听器包装

问题背景

现有通用Event基类、继承自它的KeyEvent子类,以及负责事件监听与触发的Component类:

通用Event类

// Event (Generic)
struct Event {
    // Event type
    std::string m_type;

    // c-tor
    Event(const std::string& type) : m_type(type) { /* empty */ }

    // d-tor (polymorphic)
    virtual ~Event() = default;
};

KeyEvent子类

// Event (Keyboard)
struct KeyEvent : Event {
    // Key 
    char m_key;

    // Key down or up
    bool m_down;

    // c-tor
    KeyEvent(char key, bool down) 
        : Event(down ? "keydown" : "keyup")
        , m_key(key)
        , m_down(down) { /* empty */ }
};

原始Component类

// Component (listens to events)
class Component {

    // ...

    std::map<std::string, std::vector<std::function<void(const Event&)>>> m_events;

    // ...

public:

    // Listen to event
    void listen(const std::string& type, std::function<void(const Event&)> callback) {
        m_events[type].push_back(callback);
    }

    // Trigger event
    void trigger(const Event& event) {
        auto it = m_events.find(event.m_type);
        if (it != m_events.end()) {
            for (auto& callback : m_events[event.m_type]) {
                callback(event);
            }
        }
    }
};

尝试直接传入接收KeyEvent的Lambda作为监听器时,触发编译错误:

目标用法(触发编译错误)

int main() {
    Component c;

    // Listen to key down event
    c.listen("keydown", [](const KeyEvent& keyEvent){
        std::cout << "You pressed '" << keyEvent.m_key << "'!\n";
    });

    // Trigger event
    c.trigger(KeyEvent('A', true));
}

编译错误信息

main.cpp: In function ‘int main()’:
main.cpp:65:13: error: cannot convert ‘main()::<lambda(const KeyEvent&)>’ to ‘std::function<void(const Event&)>’
   65 |     c.listen("keydown", [](const KeyEvent& event) {
      |     ~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   66 | 
      |               
   67 |     });
      |     ~~       
main.cpp:46:76: note:   initializing argument 2 of ‘void Component::listen(const string&, std::function<void(const Event&)>)’
   46 |     void listen(const std::string& type, std::function<void(const Event&)> callback) {
      |                                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~

需求:不修改main中的Lambda代码,仅修改Component::listen的参数设计,自动完成向下转型以兼容接收子类事件的回调。


解决方案

通过模板化listen函数,自动包装用户传入的回调,在内部完成Event到目标子类的安全向下转型。具体实现如下:

修改后的Component类

#include <functional>
#include <map>
#include <vector>
#include <string>
#include <iostream>
#include <type_traits>

// 辅助模板:提取可调用对象的第一个参数类型
namespace detail {
    template <typename T>
    struct first_argument;

    // 处理普通函数
    template <typename R, typename A, typename... Args>
    struct first_argument<R(A, Args...)> {
        using type = A;
    };

    // 处理成员函数(const版本)
    template <typename R, typename C, typename A, typename... Args>
    struct first_argument<R(C::*)(A, Args...) const> {
        using type = A;
    };

    // 处理Lambda和其他可调用对象
    template <typename F>
    struct first_argument : first_argument<decltype(&F::operator())> {};
}

// Event (Generic)
struct Event {
    std::string m_type;
    Event(const std::string& type) : m_type(type) {}
    virtual ~Event() = default;
};

// Event (Keyboard)
struct KeyEvent : Event {
    char m_key;
    bool m_down;
    KeyEvent(char key, bool down) 
        : Event(down ? "keydown" : "keyup"), m_key(key), m_down(down) {}
};

// Component (listens to events)
class Component {
    std::map<std::string, std::vector<std::function<void(const Event&)>>> m_events;

public:
    // 模板化的listen函数,自动处理向下转型
    template <typename Callback>
    void listen(const std::string& type, Callback&& callback) {
        using TargetEvent = typename detail::first_argument<Callback>::type;
        // 移除引用和const修饰,方便dynamic_cast
        using RawTarget = std::remove_const_t<std::remove_reference_t<TargetEvent>>;

        // 包装回调:先转型,成功再调用
        auto wrapped_callback = [callback = std::forward<Callback>(callback)](const Event& event) {
            if (const auto* derived_event = dynamic_cast<const RawTarget*>(&event)) {
                callback(*derived_event);
            }
            // 可选:转型失败时记录日志或抛出异常
            // else {
            //     throw std::bad_cast();
            // }
        };

        m_events[type].push_back(std::move(wrapped_callback));
    }

    void trigger(const Event& event) {
        auto it = m_events.find(event.m_type);
        if (it != m_events.end()) {
            for (auto& callback : it->second) {
                callback(event);
            }
        }
    }
};

验证代码(无需修改原Lambda)

int main() {
    Component c;

    // 直接传入接收KeyEvent的Lambda,无需手动转型
    c.listen("keydown", [](const KeyEvent& keyEvent){
        std::cout << "You pressed '" << keyEvent.m_key << "'!\n";
    });

    // 触发事件,自动完成转型
    c.trigger(KeyEvent('A', true));
    return 0;
}

原理说明

  1. 模板参数推导:通过模板函数listen自动推导用户传入的回调类型,再借助辅助模板detail::first_argument提取回调的参数类型(即目标事件子类)。
  2. 安全转型:在包装函数中使用dynamic_cast将const Event&转为目标子类指针,转型成功(即事件类型匹配)时才调用原回调,避免类型不匹配的错误。
  3. 完美转发:使用std::forward保留回调的左值/右值属性,避免不必要的拷贝。

内容的提问来源于stack exchange,提问作者Yan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 04:50:59