用观察者模式封装轻量通知器,维护回调列表与触发入口;注册支持std::function并用vector存储;回调中禁改观察者列表;提供set_config_silent避免递归通知;std::any需类型检查与异常处理;多线程下分层加锁并先拷贝回调列表再执行。

怎么让配置变更自动触发回调函数
直接用观察者模式封装一个轻量通知器就行,核心是维护一个回调列表 + 一个触发入口。别自己手写事件总线或依赖 Boost.Signals2 这类重型方案——对配置项这种低频、单线程、无生命周期交叉的场景,反而容易引入资源泄漏或调用栈混乱。
关键点在于:回调注册必须支持 std::function,且存储用 std::vector 而非 std::list(避免迭代器失效干扰);触发时禁止在回调里修改观察者列表(否则崩溃)。
- 用
std::shared_ptr管理回调对象?没必要,配置变更通知一般不跨线程,也不需要延长对象生命周期 - 回调签名统一为
void(const std::string& key, const std::any& old_val, const std::any& new_val),兼容任意类型值变更 - 注册接口返回一个
inttoken,用于取消订阅;内部用std::map<int std::function>></int>存储更安全,但简单场景用 vector + token 索引也够用
为什么不能在回调里改配置本身
这是最常踩的坑:某个回调里又调用了 set_config("timeout", 5000),结果触发新一轮通知,形成递归调用甚至死循环。C++ 没有 Python 那种装饰器级别的通知拦截机制,得靠设计规避。
根本原因是通知和赋值耦合在同一个函数里。正确做法是把“值变更”和“发通知”拆开:
立即学习“C++免费学习笔记(深入)”;
- 内部用一个
bool notify_enabled_ = true开关,默认开启;回调中临时设为false,改完再恢复 - 或者更干净:提供
set_config_silent()接口,只改值不发通知,回调里只调它 - 千万别用
std::recursive_mutex去包整个通知逻辑——性能差,还掩盖了设计问题
std::any 在配置通知里该怎么用才不出错
std::any 是 C++17 给配置系统提供的最小可行类型擦除工具,但它不是万能胶。用错会导致运行时 std::bad_any_cast 或静默失败。
必须配合类型检查使用,不能假设回调里一定知道值类型:
- 传入回调的
old_val和new_val都要先用old_val.type() == new_val.type()判断是否同类型,否则跳过处理 - 取值前务必用
std::any_cast<t></t>并捕获异常,或用std::any_cast<const t>(&val)</const>返回指针做空检查 - 避免在回调里对
std::any做拷贝构造(比如存到局部容器),它内部可能持有大对象,引发意外开销
多线程下怎么保证通知安全
如果配置会被多个线程读写,而通知又要跨线程执行(比如主线程更新,工作线程回调),那就不能只加个 std::mutex 锁住整个通知过程——会卡死其他线程读配置。
推荐分层保护:
- 配置存储本身用
std::shared_mutex(C++17),读多写少场景更高效 - 回调列表的增删用独立的
std::mutex,和配置锁不重叠 - 通知触发时,先拷贝当前回调列表(只读操作),再解锁,最后遍历执行——避免回调阻塞配置访问
- 不要试图在回调里加锁去同步业务状态,那是业务层该管的事;通知器只负责“喊一嗓子”,不负责“等回应”
真正难处理的是回调执行期间配置又被改了——这说明你没划清边界:通知器不该承担状态一致性责任,它只保证“变更发生后,所有已注册回调至少被调用一次”。其余,交给上层决定重试、丢弃还是排队。









