c++20的std::format如何处理自定义类型? (实现std::formatter)

9次阅读

std::formatter特化必须为const成员函数且parse()/format()分离实现;漏const或未正确解析格式说明符将导致编译失败或std::format_error。

c++20的std::format如何处理自定义类型? (实现std::formatter)

std::formatter 特化必须满足 const 限定和 parse() / format() 分离

直接在自定义类型上加 operator 或重载 std::format 不起作用——std::format 只认 std::formatter 特化。关键约束有两条:特化模板必须是 const 成员函数,且 parse()format() 必须分离实现。漏掉 const 会导致编译失败(常见错误:「no matching function for call to format」或「formatter does not satisfy formatter」)。

典型结构如下:

template <> Struct std::formatter {     constexpr auto parse(format_parse_context& ctx) -> format_parse_context::iterator {         // 解析格式说明符,如 "{}", "{:x}", "{:8}"         return ctx.end();     }     template      auto format(const MyType& t, FormatContext& ctx) const -> FormatContext::iterator {         // 写入格式化后的内容到 ctx.out()         return format_to(ctx.out(), "MyType{{val={}}}", t.val);     } };
  • parse() 必须声明为 constexpr,返回 ctx.end() 表示接受所有默认格式(若不支持任何格式说明符,可直接返回)
  • format() 参数中 const MyType&const 成员函数限定缺一不可
  • 不要在 format() 中调用 std::format 递归格式化自身字段(易触发无限模板实例化),改用 format_to

处理格式说明符(如 “{:x}”、”{:8}”)需手动解析 ctx.input()

format_parse_context::iterator 实际是指向格式字符串{} 内容的迭代器,比如 "{:04x}"ctx.begin() 指向 '0'。你得自己跳过空格、识别前缀、提取数字等——std::formatter 不提供现成解析器。

例如支持 "x"(十六进制)和宽度(如 "8"):

立即学习C++免费学习笔记(深入)”;

constexpr auto parse(format_parse_context& ctx) -> format_parse_context::iterator {     auto it = ctx.begin();     auto end = ctx.end();     if (it != end && *it == 'x') {         hex_ = true;         ++it;     }     if (it != end && std::isdigit(*it)) {         width_ = static_cast(*it - '0');         ++it;     }     return it; // 必须返回解析结束位置 }
  • 未识别的字符不能忽略——必须停在第一个非法位置,否则 std::format 会报 std::format_error
  • 宽度建议存为 intstd::size_t 成员变量,供 format() 使用;不要在 format() 中重新解析
  • 不支持的说明符(如 "f")应让 parse() 报错:抛出 std::format_error("unknown format specifier")

嵌套格式化字段(如 struct 成员)要用 format_to + std::make_format_args

MyTypeint id;std::String name;,不能写 format_to(ctx.out(), "{} {}", t.id, t.name) —— 这会尝试调用 std::formatter 等,但它们不是你特化的类型,且上下文类型不匹配。

正确做法是显式构造参数包并复用底层格式设施:

template  auto format(const MyType& t, FormatContext& ctx) const -> FormatContext::iterator {     if (hex_) {         return format_to(ctx.out(), "MyType{{id={:x}, name={}}}", t.id, t.name);     } else if (width_ > 0) {         return format_to(ctx.out(), "MyType{{id={:0{}}d, name={}}}", t.id, width_, t.name);     }     return format_to(ctx.out(), "MyType{{id={}, name={}}}", t.id, t.name); }
  • format_to 是安全的,它接受任意已支持类型的参数(intstd::stringstd::string_view 等)
  • 注意 {:0{}}d 这种嵌套占位符:第二个 {} 会从参数列表取 width_ 值,用于动态指定宽度
  • 所有字段类型必须本身支持 std::formatter(内置类型和标准容器基本都支持,自定义类型则需另行特化)

特化必须在使用前可见,且不能在匿名命名空间

最常踩的坑:把 std::formatter 特化放在 .cpp 文件里,或包在 Namespace { } 中。链接期找不到特化,运行时报 std::format_error 或编译失败。

  • 特化必须与 std::formatter 在同一命名空间(即全局命名空间),且在首次调用 std::format 前完成声明和定义
  • 推荐放在头文件中,紧挨着 MyType 定义之后,或至少确保包含顺序正确
  • 如果 MyType 在命名空间 ns 中,特化仍必须写成 template struct std::formatter<:mytype char>,不能写 namespace ns { template struct std::formatter<...> }

复杂点在于 parse() 的健壮性和 format() 中对上下文输出器的精确控制——稍有不慎就会格式错乱或崩溃,而不是优雅降级。

text=ZqhQzanResources