c++oding="utf-8" ?>
应使用模板参数抽象事件类型,通过std::type_index+std::any实现注册期类型擦除、通知期零开销调用;避免void*或基类指针,禁用std::variant聚合事件,保持attach/notify接口简洁,生命周期管理需额外处理。

观察者接口怎么用模板参数抽象事件类型
直接让 Observer 模板接受事件类型(比如 struct UserLoginEvent),而不是用 void* 或基类指针,能避免运行时类型擦除和 dynamic_cast 开销。关键在于把事件类型作为模板参数传入,同时允许不同事件共存于同一观察者管理器中——这需要类型擦除层,但擦除发生在注册阶段,不是通知阶段。
常见错误是试图用单一模板实例承载所有事件:Observer<eventbase></eventbase>,结果导致所有回调必须处理基类,丢失编译期类型信息。正确做法是每个具体事件类型生成独立的 Observer<t></t> 实例,再由一个非模板容器(如 std::unordered_map<const std::type_info std::any></const>)统一管理它们。
- 注册时用
typeid(T).name()作键,把std::function<void t></void>存进std::any - 通知时按事件类型查表,
std::any_cast还原函数对象,直接调用——无虚函数、无 RTTI 判定 - 注意
std::type_info::name()不跨编译单元唯一,应改用typeid(T).hash_code()或std::type_index
如何用 std::function + std::any 实现类型安全的通知分发
核心难点不是“怎么存”,而是“怎么在不暴露模板参数的情况下安全取出并调用”。std::any 本身不提供类型转换保障,std::any_cast 失败会抛 std::bad_any_cast,所以必须确保存取类型严格一致。
典型场景:用户注册了 onUserLogin 回调监听 UserLoginEvent,但系统误向该槽位投递 UserLogoutEvent——这不该静默失败,而应在编译期或注册期拦截。
- 注册函数签名应为
template<typename t> void attach(std::function<void t> f)</void></typename>,强制类型明确 - 内部存储用
std::unordered_map<:type_index std::any></:type_index>,键由std::type_index{typeid(T)}构造 - 通知函数
template<typename t> void notify(const T& event)</typename>中先查表,再std::any_cast<:function t>>(slot)</:function>,失败说明注册/通知类型不匹配,应 assert 或日志告警
为什么不能直接用 std::variant 替代 std::any 做事件聚合
std::variant 要求编译期穷举所有可能事件类型,一旦新增事件就得改模板参数列表,违背“通用解耦”目标。它适合已知有限且稳定事件集的场景(如状态机),但不适合作为观察者总线的底层容器。
有人尝试写 using EventVariant = std::variant<userloginevent userlogoutevent ...></userloginevent>,然后让所有观察者监听这个大 variant——这导致每个回调都得写 std::visit 分支,丧失事件类型的语义隔离,也破坏了单一职责。
-
std::any允许运行时动态扩展事件类型,只要注册时类型匹配即可 - 性能上,
std::any的存储开销略高(含 type_info 指针),但现代 libc++/MSVC 对小对象有 SSO 优化,实际差异可忽略 - 真正瓶颈不在存储,而在哈希查找和函数调用跳转——这部分无法避免,也不该试图绕过
模板元编程在这里只负责接口泛化,别让它侵入实现逻辑
所谓“基于模板元编程”,其实仅体现在 attach 和 notify 的模板声明上,以及用 std::type_index 做类型标识。不需要 std::enable_if、SFINAE 或 constexpr if 等复杂技巧——那些会让代码变成类型检查器,而不是观察者。
容易踩的坑是过度设计:比如用 std::is_invocable_v<f const t></f> 约束回调类型,看似严谨,实则限制了 lambda 捕获、成员函数指针等合法用法;或者为支持 const/volatile 修饰搞多层模板偏特化,最终没人能维护。
- 保持
attach接口简单:template<typename t typename f> void attach(F&& f)</typename>,靠std::function构造函数做隐式转换 - 不验证
F是否真的可调用——构造std::function失败时编译器自然报错,更清晰 - 如果真要编译期校验,用
static_assert(std::is_invocable_v<f const t>)</f>放在函数体内,而非模板约束,避免推导失败时错误信息晦涩
最麻烦的从来不是类型系统,而是生命周期管理:谁拥有回调?观察者是否需弱引用?这些没法靠模板解决,得靠明确约定或 RAII wrapper。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











