可行但需规避三类高频事故:类型擦除后无法校验、跨模块传参崩溃、回调中删自己致迭代器失效;std::any_cast失败不应仅靠catch异常,因其开销大且掩盖契约断裂问题,应通过显式模板参数标注类型并用safe_cast指针方式校验。

直接用 std::any 实现消息总线可行,但必须避开类型擦除后无法校验、跨模块传参崩溃、回调中删自己导致迭代器失效这三类高频事故。
为什么 std::any_cast 失败时不能只 catch 异常?
异常捕获在 hot path 上开销大,且掩盖了契约断裂问题——调用方本该知道发的是什么类型,而不是靠 try/catch 猜。更糟的是,std::bad_any_cast 一旦抛出,说明监听器注册类型和发布类型不一致,属于编译期/设计期错误,不该留到运行时才发现。
- 发布前务必显式标注类型:用
bus.publish<clickevent>(e)</clickevent>,而非bus.publish(e)(后者依赖模板参数推导,易因 cv-qualifier 或引用性丢失导致any_cast失败) - 调试阶段可在
subscribe内部加断言:assert(message.type() == typeid(MessageType)) - 生产环境建议封装一层
safe_cast函数,用std::any_cast<messagetype>(&message)</messagetype>拿指针,返回nullptr表示类型不匹配,比异常更轻量可控
std::map<:type_index std::vector>> 的线程安全陷阱
原实现里所有操作都加 std::mutex,看似稳妥,实则扼杀了读多写少场景下的性能。注册/注销频率远低于触发频率,锁整个 publish 流程是过度保护。
- 触发时不锁,但需确保触发期间容器不被修改:可在
publish开头对 handler 向量做一次std::vector::data()快照(即拷贝原始指针数组),后续遍历用快照,避免迭代中 resize 崩溃 - 若需支持“回调内注销自己”,禁止边遍历边
erase;改用标记位 + 二次清理,或换std::list(其erase返回下一节点迭代器) - 跨线程发布时,回调对象生命周期必须长于调用:监听者应持
std::shared_ptr<self></self>,并在回调开头用weak_ptr::lock()判断是否仍存活
std::any 能不能跨 .so / .dll 边界传递?
不能。不同编译器或 STL 版本的 std::any 内存布局不兼容,尤其在 Windows DLL 或 macOS dylib 中,std::any{42} 在模块 A 中构造,在模块 B 中 any_cast<int></int> 极大概率崩溃。
立即学习“C++免费学习笔记(深入)”;
- 限制
std::any使用范围:仅限单编译单元、静态链接库,或同一进程内由同一 STL 实例管理的模块 - 跨模块通信请改用稳定 ABI 的中间表示:例如定义固定结构体
struct MessageHeader { uint16_t type_id; uint32_t payload_size; }+ 序列化协议(Protobuf/FlatBuffers) - 若坚持用
std::any解耦,优先考虑std::variant<int std::string double></int>—— 类型集合有限、可枚举、无 ABI 风险,且编译期就能检查覆盖度
真正难的不是把消息发出去,而是让接收方确信它拿到的就是自己声明要处理的那个类型——这需要类型契约写进接口签名里,而不是靠运行时碰运气。


















