如何让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; }
原理说明
- 模板参数推导:通过模板函数
listen自动推导用户传入的回调类型,再借助辅助模板detail::first_argument提取回调的参数类型(即目标事件子类)。 - 安全转型:在包装函数中使用
dynamic_cast将const Event&转为目标子类指针,转型成功(即事件类型匹配)时才调用原回调,避免类型不匹配的错误。 - 完美转发:使用
std::forward保留回调的左值/右值属性,避免不必要的拷贝。
内容的提问来源于stack exchange,提问作者Yan
相关产品推荐
相关产品推荐

