更换日志库时将spdlog的{}占位符转为标准格式说明符的需求
这个场景我太熟了!之前换日志库的时候,就因为spdlog的{}和标准printf风格格式符不兼容卡了好久。给你整理了几个实用的方案,按需选择就行:
方案一:编译期类型安全的格式符替换(推荐)
因为你的Log是模板函数,可以利用C++的类型推导和模板元编程,在编译期自动把{}替换成对应类型的标准格式说明符,既安全又没运行时开销。
首先写一个类型映射的trait,把每个常用类型对应到正确的格式符:
#include <type_traits> #include <string> // 类型到标准格式符的映射 template<typename T> struct FormatSpecifier; // 为常用类型特化 template<> struct FormatSpecifier<int> { static constexpr const char* value = "%d"; }; template<> struct FormatSpecifier<long> { static constexpr const char* value = "%ld"; }; template<> struct FormatSpecifier<unsigned long> { static constexpr const char* value = "%zu"; }; template<> struct FormatSpecifier<const char*> { static constexpr const char* value = "%s"; }; template<> struct FormatSpecifier<std::string> { static constexpr const char* value = "%s"; }; template<> struct FormatSpecifier<float> { static constexpr const char* value = "%f"; }; template<> struct FormatSpecifier<double> { static constexpr const char* value = "%lf"; }; // 重构后的Log模板函数 template<typename... Args> void Log(const char* fmt, const Args&... args) { std::string standard_fmt = fmt; size_t current_pos = 0; // 用折叠表达式逐个替换占位符 auto replace_placeholder = [&](const auto& arg) { current_pos = standard_fmt.find("{}", current_pos); if (current_pos == std::string::npos) return; using ArgType = std::decay_t<decltype(arg)>; standard_fmt.replace(current_pos, 2, FormatSpecifier<ArgType>::value); current_pos += strlen(FormatSpecifier<ArgType>::value); }; (replace_placeholder(args), ...); // 调用新日志库的printf风格接口 g_NewLogger->log(standard_fmt.c_str(), args...); }
这个方案的核心是编译期类型检查,如果你的代码里传了没有对应特化的类型,编译器会直接报错,避免了运行时格式不匹配的坑。唯一需要做的就是给项目里用到的所有类型补全特化,常用类型也就那几种,工作量不大。
方案二:运行时灵活适配(适合类型复杂的场景)
如果你的参数类型特别多,或者不想写一堆特化,可以用if constexpr在编译期判断类型,动态替换格式符——比纯运行时的typeid高效,还不用开RTTI:
#include <string> #include <type_traits> template<typename... Args> void Log(const char* fmt, const Args&... args) { std::string standard_fmt = fmt; size_t current_pos = 0; auto replace_placeholder = [&](const auto& arg) { current_pos = standard_fmt.find("{}", current_pos); if (current_pos == std::string::npos) return; const char* spec = nullptr; using ArgType = std::decay_t<decltype(arg)>; if constexpr (std::is_same_v<ArgType, int>) { spec = "%d"; } else if constexpr (std::is_same_v<ArgType, unsigned int>) { spec = "%u"; } else if constexpr (std::is_same_v<ArgType, unsigned long>) { spec = "%zu"; } else if constexpr (std::is_same_v<ArgType, const char*> || std::is_same_v<ArgType, std::string>) { spec = "%s"; } else if constexpr (std::is_floating_point_v<ArgType>) { spec = "%f"; } else { // 未知类型默认转成字符串用%s spec = "%s"; // 这里可以加个分支,把自定义类型转成std::string,比如重载to_string } standard_fmt.replace(current_pos, 2, spec); current_pos += strlen(spec); }; (replace_placeholder(args), ...); g_NewLogger->log(standard_fmt.c_str(), args...); }
这种方式灵活性更高,遇到自定义类型时可以额外处理(比如转成字符串),缺点是如果漏了某个类型的判断,可能会出现格式错误,需要多做测试覆盖。
方案三:用fmt库做中间层(最省心的偷懒办法)
如果你的项目里已经引入了fmt库(毕竟spdlog底层就是靠它),直接用它把{}格式的字符串转成最终的日志文本,再传给新日志库就行——完全不用管格式符的问题:
#include <fmt/core.h> template<typename... Args> void Log(const char* fmt, const Args&... args) { // 用fmt把spdlog风格的格式字符串直接转成成品日志 std::string log_content = fmt::format(fmt, args...); // 传给新日志库(假设新库接受现成的字符串) g_NewLogger->log(log_content.c_str()); }
这个方案几乎零侵入现有代码,连Log函数的调用方式都不用改。唯一的小缺点是如果新日志库本身支持格式化,相当于做了两次格式化,有一点点性能开销,但大部分业务场景下完全可以忽略。
注意点
- 如果你用的是C++14及更早版本,没法用折叠表达式
(replace_placeholder(args), ...),需要用递归模板来展开参数包。 - 自定义类型一定要单独处理,要么给它特化
FormatSpecifier,要么实现转字符串的逻辑,避免格式错误。 - 测试时要覆盖所有常用类型,比如
unsigned long必须用%zu而不是%u,不然会出现数值溢出或者打印错乱的问题。
内容的提问来源于stack exchange,提问作者Aminos
相关产品推荐
相关产品推荐

