C++如何实现简单的字符串格式化类_C++模仿Python中format语法【工具】

7次阅读

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

C++如何实现简单的字符串格式化类_C++模仿Python中format语法【工具】

为什么不用 std::format 或 fmt::format

因为 c++20 的 std::format 在 MSVC 19.3x 之前不完整,GCC 13 以下默认不启用,Clang 更晚;而第三方 fmt::format 虽好,但引入整个库对轻量工具类来说太重。你需要的是一个仅依赖标准库、头文件即用、支持位置占位符(如 {0}{1})和命名占位符(如 {name})的最小实现。

核心思路:两次遍历 + 状态机解析

不能靠正则(标准库无原生支持),也不宜递归展开。实际做法是:第一次扫描字符串,提取所有 {...} 片段,记录起始位置、类型(位置型/命名型)、参数索引或键名;第二次按顺序拼接——遇到普通文本直接追加,遇到占位符则查参数表并格式化(调用 std::to_Stringstd::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 字符干扰索引),这些细节往往比主逻辑更耗调试时间。

text=ZqhQzanResources