最稳方式是用spdlog层级式logger名+get()懒注册,因其天然支持命名空间结构、父子继承与独立sink绑定,避免手动解析带来的边界问题。

用 spdlog 按命名空间自动分 logger,别手写工厂
直接结论:C++ 里实现带命名空间的日志分类,最稳的方式是用 spdlog 的层级式 logger 名称 + spdlog::get() 懒注册机制,而不是自己封装命名空间映射表或宏展开。
原因很简单:spdlog 本身就把 logger 名称当路径处理(比如 "network.http.client"),支持父子继承、级别传播、独立 sink 绑定——这天然匹配 C++ 命名空间结构。你硬要自己解析 foo::bar::Baz 字符串再映射,反而容易漏掉模板实例化、匿名命名空间、内联命名空间等边界情况。
实操建议:
- 把命名空间路径转成点分 logger 名,例如
namespace foo::bar { void func() { auto lg = spdlog::get("foo.bar"); } } - 全局只调用一次
spdlog::set_level()控制默认级别,各模块用spdlog::get("xxx")->set_level()覆盖 - 避免在头文件里直接调用
spdlog::get(),防止多定义;统一放在 .cpp 首行或首次使用前初始化 - 不要用
spdlog::stdout_logger_mt("foo::bar")这种方式手动创建——名字含::会被当字面量,失去层级关系
为什么不用 __PRETTY_FUNCTION__ 或 __func__ 自动生成 logger 名
看起来很美:自动提取当前作用域,省得写字符串。但实际踩坑率极高。
立即学习“C++免费学习笔记(深入)”;
常见错误现象:__PRETTY_FUNCTION__ 在模板函数里会包含完整类型签名(如 void foo::bar::process<int std::string>()</int>),日志系统瞬间生成几百个不同名字的 logger,内存泄漏+查找变慢;__func__ 又太短,跨命名空间同名函数(比如多个 init())全挤在一个 logger 下,失去分类意义。
更现实的做法是显式声明模块粒度,而非函数粒度:
- 每个 .cpp 文件顶部定义静态 logger 引用:
static auto& lg = *spdlog::get("network.tcp.server"); - 如果真要自动化,用宏限定范围:
#define MODULE_LOGGER(name) static auto& lg = *spdlog::get(name),然后在文件开头写MODULE_LOGGER("storage.sqlite"); - 禁止在循环或 hot path 里反复调用
spdlog::get()——它内部有哈希查找,虽快但非零开销
spdlog::get() 返回空指针?检查初始化顺序和线程安全
典型错误信息:std::bad_weak_ptr 或解引用空指针崩溃,尤其在静态对象构造函数里调用 spdlog::get() 时。
根本原因是:spdlog 的全局 logger 注册表本身是延迟初始化的,依赖第一次调用 spdlog::default_logger() 或显式 spdlog::create() 触发。而静态对象构造顺序跨编译单元不可控,早于 spdlog 初始化就调用 get(),必然返回 nullptr。
解决方案很具体:
- 所有 logger 获取逻辑,必须晚于
spdlog::default_logger()第一次调用(比如 main() 开头、或配置加载后) - 不依赖全局静态 logger 对象,改用函数局部静态变量封装:
auto& get_network_logger() { static auto l = spdlog::get("network"); return l; } - 确认没在
main()外(如全局变量初始化器、DLL 加载钩子)调用任何 spdlog 接口 - Windows 下若用 DLL,确保 spdlog 库和调用方链接同一份 CRT,否则静态变量各自一份,
get()查不到对方创建的 logger
自定义 sink 时如何保留命名空间上下文
如果你写了自定义 sink(比如发到远程日志服务),发现收不到 logger 名或无法按命名空间分流,大概率是没从 spdlog::details::log_msg 里取对字段。
关键点:logger_name 字段就是你传给 spdlog::get() 的那个字符串,不是类名也不是函数名;它在 log_msg.logger_name 里,且已去重、可直接用于路由判断。
实操注意:
- 不要试图从
log_msg.source(文件/行号)反推命名空间——C++ 没标准方式从源码位置还原 namespace 层级 - 如果需要更细粒度(如区分 class 内 method),靠
log_msg.logger_name+ 自定义属性(lg->info("{}", SPDLOG_FMT_STRING("method=send")))组合实现 - 多线程下,
log_msg.logger_name是 const char*,指向内部字符串池,无需额外拷贝,可直接 strcmp 或哈希 - 别在 sink 里调用
spdlog::get()——可能触发递归初始化,尤其在异常处理路径中
命名空间日志真正的复杂点不在创建,而在跨模块调用时的 logger 传递与复用。比如 A 模块创建了 "db.pool" logger,B 模块想复用它而不是新建一个同名 logger,就得确保两者链接的是同一个 spdlog 实例,且没被 ODR-violation 拆成两份——这比设计分类逻辑更容易出静默问题。










