C++中“绝对零开销”不可实现,但可通过编译期确定调用、禁用虚函数/堆分配/RTTI/锁、用if constexpr分发、SOA状态管理及显式对齐逼近零开销。

绝对零开销在 C++ 里根本不存在,但可以逼近
所谓“绝对零开销”是误导性说法——只要调用函数、访问内存、做类型擦除或动态分发,就一定有成本。真正可追求的是:**编译期确定调用目标、无虚函数/堆分配/锁/RTTI/动态内存管理、无分支预测失败惩罚**。这意味着你必须放弃通用性换性能:不能支持任意参数类型、不能运行时注册、不能跨线程自动同步。
典型错误是直接套用 std::function + std::vector 实现事件总线,这会触发堆分配和间接调用;更糟的是加 std::mutex 做线程安全——锁本身不是开销最大头,而是让 CPU 流水线停摆、缓存行失效。
- 所有事件类型必须是编译期已知的
struct,带唯一static constexprID - 监听器必须是函数对象(
autolambda 或structwithoperator()),不能是std::function - 注册必须在编译期或启动期完成,禁止运行时
push_back到全局容器 - 不提供跨线程投递能力;若需多线程,由用户自己用无锁队列 + 手动 dispatch
用 std::array + if constexpr 消除运行时分支
传统 switch 或 map 查找事件类型,会产生分支或哈希计算。换成模板特化 + if constexpr,整个分发逻辑在编译期展开为直序代码,CPU 不跳转、不预测、不失败。
示例:定义事件基类与派生
struct EventBase { static constexpr int id = 0; };
struct MouseMove : EventBase { int x, y; static constexpr int id = 1; };
struct KeyPress : EventBase { char key; static constexpr int id = 2; };
分发函数利用 if constexpr 展开:
template<typename e>
void dispatch(const E& e) {
if constexpr (E::id == MouseMove::id) {
for (auto& h : mouse_handlers) h(e);
} else if constexpr (E::id == KeyPress::id) {
for (auto& h : key_handlers) h(e);
}
}
</typename>
- 每个事件类型对应独立的
std::array存储其监听器,避免类型擦除 -
mouse_handlers和key_handlers是std::array,大小编译期固定,迭代无边界检查开销 - 不写成模板参数包展开(如
dispatch_impl),否则会导致实例化爆炸和编译慢
监听器存储必须是 SOA 或 AOS+对齐,禁用 std::function
std::function 内部有小缓冲优化,但一旦捕获变量超限,立刻触发堆分配;且调用需通过函数指针跳转,破坏内联机会。高性能路径只接受两种监听器:
- 自由函数指针:
void(*)(const T&)—— 可内联,无状态 - 无捕获 lambda:
[](const T&){...}—— 编译器视为函数指针,同上
若需状态,用 SOA 方式分离数据与行为:把状态存进预分配的 std::array,监听器只存索引或引用。例如:
struct PlayerInputHandler {
void operator()(const KeyPress& e) {
auto& state = player_states[index]; // index 是编译期绑定的常量
if (e.key == 'W') state.vel.y = 1.0f;
}
size_t index;
};
这样避免了 vtable、避免了 heap alloc、避免了 indirect call,且 player_states 可按 cache line 对齐。
别碰 std::any、std::variant 和运行时类型识别
哪怕只用一次 std::any_cast 或 std::get_if,都会引入运行时类型比较、可能的异常路径、以及无法内联的间接访问。有人试图用 std::variant 统一封装事件,结果发现 std::visit 生成的跳转表比 if constexpr 更重——尤其当事件类型超过 4 种时,分支预测失败率飙升。
真正轻量的做法是:不同事件走不同函数入口。不是「一个 publish 函数塞所有事件」,而是
publish_mouse_move(MouseMove{10, 20});
publish_key_press(KeyPress{'A'});
- 每个 publish 是独立 inline 函数,直接调用对应 handler 数组
- 没有统一事件基类继承体系,避免虚析构、vptr 开销
- 如果真要统一接口,用 ADL + tag dispatch,而不是运行时多态
最易被忽略的一点:**所有数组尺寸必须是编译期常量,且显式指定对齐(alignas(64))以避免 false sharing**。否则两个核心往同一 cache line 写,性能掉一半。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











