用std::function+std::map实现事件注册与触发的核心是:以std::string为键、std::vector<std::function<void(const EventData&)>>为值构建映射,注册时emplace_back值拷贝回调,触发前检查键存在性,遍历时避免边遍历边erase,跨线程场景下对注册/注销加锁、触发可无锁拷贝回调列表执行。

如何用 std::function + std::map 实现事件注册与触发
核心就是用 std::map 存事件名到回调函数列表的映射,每个事件对应一个 std::vector<:function>> </:function>。别用裸指针或全局单例,容易内存泄漏或生命周期错乱。
常见错误是把 std::function 存成引用或临时对象——它必须值拷贝;还有人用 std::unordered_map 但没自定义哈希,导致编译失败。
-
std::string作事件名最稳妥,避免用枚举+转换增加耦合 - 回调签名统一为
void(const EventData&),用结构体封装参数比可变参数更安全 - 注册时用
emplace_back插入,避免重复拷贝std::function - 触发前检查
map.find(key) != end(),空事件不报错也不 crash
为什么不能在回调里调用 unregister(移除自己)
正在遍历 std::vector 时 erase 当前迭代器,会导致迭代器失效,后续访问越界——这是典型的“边遍历边修改”问题,不是线程安全问题,而是容器本身行为。
实际场景比如 UI 组件监听 “window_close” 事件,回调里想把自己从事件系统注销,直接删会崩。
立即学习“C++免费学习笔记(深入)”;
- 解决方案:加个
std::vector<:string> to_remove</:string>缓存待删事件名,遍历完再批量清理 - 或者改用
std::list配合erase返回的迭代器,但性能略差 - C++20 可用
std::erase_if,但得确保所有回调执行完才清理
如何支持跨线程发布但避免锁竞争
纯锁(如 std::mutex)包住整个 trigger 函数,会卡住所有事件分发;锁粒度太粗,尤其高频事件(如帧更新)下性能骤降。
真正要保护的只是“注册/注销”操作,而“触发”本身可以无锁——只要保证回调列表在触发时不被修改即可。
- 用
std::shared_mutex:写注册/注销用lock,读触发用shared_lock - 或者把回调列表拷贝一份再遍历:
auto callbacks = m_events.at(event_name);,之后对原容器的操作不影响本次触发 - 注意拷贝
std::function有开销,高频小事件建议用轻量回调(如void(*)())或 move 语义优化
std::any 能不能用来传任意事件参数
能,但别滥用。用 std::any 包一层看似灵活,实际让类型检查退到运行时,调试困难,且每次 any_cast 失败会抛异常。
更适合的方案是定义几个常用事件结构体:MouseClickEvent、KeyEvent,再用 std::variant 统一入口,编译期就能约束类型。
- 如果真要用
std::any,务必在 trigger 前加if (e.type() == typeid(MyEvent))判断,别裸 cast - 避免在 hot path(如每帧调用)中用
std::any,构造/析构开销明显高于 POD 结构体 - 比起
std::any,更推荐模板化 event dispatcher,不同事件类型走不同注册表
事件系统最难的不是发消息,而是生命周期管理——谁负责销毁回调里的 this 指针,谁保证事件名字符串不提前释放。这些细节不处理好,跑几天就 core dump。


















