不使用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 字符干扰索引),这些细节往往比主逻辑更耗调试时间。










