不使用std::format或fmt::format因其兼容性差或过于重量级,需一个仅依赖标准库、支持位置/命名占位符的轻量实现,核心是两次遍历与状态机解析。

为什么不用 std::format 或 fmt::format
因为 c++20 的 std::format 在 MSVC 19.3x 之前不完整,GCC 13 以下默认不启用,Clang 更晚;而第三方 fmt::format 虽好,但引入整个库对轻量工具类来说太重。你需要的是一个仅依赖标准库、头文件即用、支持位置占位符(如 {0}、{1})和命名占位符(如 {name})的最小实现。
核心思路:两次遍历 + 状态机解析
不能靠正则(标准库无原生支持),也不宜递归展开。实际做法是:第一次扫描字符串,提取所有 {...} 片段,记录起始位置、类型(位置型/命名型)、参数索引或键名;第二次按顺序拼接——遇到普通文本直接追加,遇到占位符则查参数表并格式化(调用 std::to_String 或 std::ostringstream)。关键点:
-
{必须成对出现,连续两个{{视为字面量{ - 位置参数如
{0}中的0必须是非负整数,超出传入参数数量则抛std::out_of_range - 命名参数如
{user}需要传入std::map<:string std::string>或类似结构,未找到键时行为应明确(建议抛异常而非静默忽略) - 不支持对齐、精度等格式说明符(如
{:6}),那是fmt层级的事
简易实现示例(仅支持位置参数)
class SimpleFormatter { public: template static std::string format(const std::string& fmt, Args&&... args) { std::vector args_vec = {std::to_string(std::forward(args))...}; std::string result; size_t i = 0; while (i < fmt.size()) { if (fmt[i] == '{' && i + 1 < fmt.size() && fmt[i + 1] == '{') { result += '{'; i += 2; } else if (fmt[i] == '}' && i + 1 < fmt.size() && fmt[i + 1] == '}') { result += '}'; i += 2; } else if (fmt[i] == '{') { size_t end = fmt.find('}', i); if (end == std::string::npos) throw std::runtime_error("unmatched '{'"); std::string content = fmt.substr(i + 1, end - i - 1); try { size_t idx = std::stoul(content); if (idx >= args_vec.size()) throw std::out_of_range("index out of range"); result += args_vec[idx]; } catch (const std::exception&) { throw std::runtime_error("invalid placeholder: {" + content + "}"); } i = end + 1; } else { result += fmt[i++]; } } return result; } };
用法:SimpleFormatter::format("Hello {0}, you have {1} messages", "Alice", 5) → "Hello Alice, you have 5 messages"。注意它不处理 int 以外类型(如 double 或自定义类),若需通用,得配合 std::ostringstream 替代 std::to_string。
命名参数支持的关键改动点
要支持 {name},必须把参数从变参包转为键值映射。最简方式是要求用户显式传入 std::unordered_map<:string std::string>,并在解析到 {xxx} 且内容非数字时查表。但要注意:
立即学习“Python免费学习笔记(深入)”;
- 不能和位置参数混用(否则歧义),要么全用位置,要么全用命名
-
{0}和{name}同时存在时,解析逻辑需先判断是否纯数字,再 fallback 到 map 查找 - 若使用
std::any存储参数值(C++17+),可支持任意可流输出类型,但会增加编译时间和二进制体积 - 实际项目中,更推荐用宏封装:比如
FORMAT("Hi {name}", _name="Bob"),内部用预处理器生成临时 map,避免手写冗长初始化
真正难的不是解析,而是错误提示是否友好、边界情况是否覆盖(比如空占位符 {}、嵌套大括号、Unicode 字符干扰索引),这些细节往往比主逻辑更耗调试时间。