不能直接用std::function+vector存观察者,因其类型擦除导致两次堆分配且无法编译期校验回调签名匹配性,易引发运行时崩溃;模板元编程将订阅关系移至编译期,通过tuple、constexpr if和is_invocable_v实现零开销、强类型安全的事件系统。

为什么不能直接用 std::function + vector 存观察者
因为类型擦除带来两次堆分配(std::function 构造 + vector::push_back 扩容),且无法在编译期校验事件参数与回调签名是否匹配。实际项目中,你改了一个事件参数,却要等到运行时触发才报 std::bad_function_call 或静默崩溃——这不是解耦,是埋雷。
模板元编程在这里不是炫技,是把“谁订阅了什么事件”这个关系移到编译期:每个事件类型 E 对应唯一一个观察者列表类型,回调必须显式匹配 void(E&) 或 bool(E&) 签名,否则直接编译失败。
- 用
std::tuple代替std::vector存不同类型的观察者函数对象(含 lambda、成员函数指针、functor) - 用
constexpr if+std::is_invocable_v在on注册时做签名检查 - 避免任何运行时类型擦除——所有类型信息保留在模板参数里
如何让事件派发支持返回值聚合与短路
很多框架只支持「全部执行完」或「手动 break」,但真实业务需要:比如权限类事件,只要一个观察者返回 false 就该立刻终止;又比如聚合统计类事件,需要收集所有观察者的返回值。
关键在事件基类设计:定义 using result_type = bool 或 using result_type = std::optional<int></int>,再用折叠表达式实现两种策略:
template<typename e>
struct event_base {
using result_type = void;
static constexpr bool short_circuit = false;
};
// 权限事件:任意 false 则停止
struct auth_event : event_base<auth_event> {
using result_type = bool;
static constexpr bool short_circuit = true;
};</auth_event></typename>
- 派发函数用
(... && invoke_observer<i>(e))</i>实现逻辑与短路(result_type=bool) - 聚合场景用
std::array<result_type n></result_type>预分配空间,避免 vector 动态扩容 - 注意:lambda 捕获引用时生命周期必须长于事件系统——常见坑是栈上对象注册后被析构
怎么处理成员函数作为观察者时的 this 指针绑定
不能依赖 std::bind(它产生类型擦除)或 std::shared_ptr(引入引用计数开销)。正确做法是把成员函数指针和对象指针拆成两个模板参数,在 tuple 中存为 observer<t r></t> 形式。
注册接口写成:on<myevent>(&MyClass::handle, &obj)</myevent>,其中 &obj 必须是左值引用或指针,编译器据此推导 T 类型。
- 禁止传临时对象地址:
on<e>(&X::f, &X{})</e>→ 编译期报错(cannot bind rvalue to lvalue reference) - 若需转移所有权,显式用
std::unique_ptr包裹并重载调用操作符,不隐藏内存语义 - 静态成员函数可直接当普通函数处理,无需特殊路径
如何避免模板膨胀导致编译时间爆炸
每个事件类型生成一套独立的 tuple 和派发逻辑,100 个事件可能让 obj 文件翻倍。核心压缩点在「观察者存储」和「派发入口」:
- 把通用逻辑(如遍历 tuple、参数转发)抽到非模板基类,用
void*+size_t存元数据,仅模板部分负责类型安全的调用 - 用
extern template显式实例化高频事件(如on<mouse_click></mouse_click>),阻止重复生成 - 禁用 RTTI:所有类型检查走
std::is_same_v和std::is_invocable_v,不依赖typeid
真正难的是生命周期管理——框架不持有观察者对象,也不干预其销毁顺序。谁注册谁负责确保对象存活,这点没法靠模板自动解决。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











