std::formatter全特化必须在std命名空间内、显式指定char参数、parse返回合法位置、format禁用递归std::format且用format_to;否则编译失败或崩溃。

不特化 std::formatter,std::format 对任何自定义智能指针或含私有/嵌套成员的复合对象都必然编译失败;真正能跑通的关键不是“写对了 format”,而是“parse 返回位置严格、format 不递归调用 std::format、特化位置和可见性完全匹配编译器查找规则”。
std::formatter 全特化必须在 std 命名空间内且不可省略 char 参数
很多人为 std::shared_ptr<mystruct></mystruct> 写 formatter 时直接在全局命名空间里 template struct formatterstd::format 只查 std::formatter<t char></t> 的全特化,且必须在 namespace std { } 里。漏掉 , char 或写成 template<typename t> struct formatter<shared_ptr>></shared_ptr></typename>(偏特化)都会导致 no matching function 错误。
正确写法必须是:
namespace std {
template<> struct formatter<std::shared_ptr<MyStruct>, char> {
constexpr auto parse(format_parse_context& ctx) { return ctx.end(); }
template <typename FormatContext>
auto format(const std::shared_ptr<MyStruct>& ptr, FormatContext& ctx) const {
if (!ptr) return format_to(ctx.out(), "nullptr");
return format_to(ctx.out(), "SP[{}]", *ptr);
}
};
}
- 特化必须显式写出两个模板参数:
std::shared_ptr<mystruct></mystruct>和char(宽字符场景才换wchar_t) - 不能把特化放在
MyStruct定义所在的头文件末尾就完事——如果shared_ptr<mystruct></mystruct>在其他模块中被格式化,该头文件必须被包含,否则特化不可见 - MSVC 19.32+、GCC 13.3+、Clang 15.0+ 才稳定支持;GCC 13.2 之前需加
-D_GLIBCXX_USE_CXX11_ABI=1避免 libstdc++ 实验实现干扰
parse() 返回非法字符位置,不是 ctx.end() 就是崩溃源头
为支持 {:deref} 或 {:raw} 这类自定义说明符,parse() 必须逐字符校验并返回第一个非法位置。返回 ctx.end() 表示“全部合法”,但若用户写了 {:deref8},而你的 parse 只认 :deref,却返回 ctx.end(),后续 format() 会读到后面的 8 并可能触发越界或未定义行为。
立即学习“C++免费学习笔记(深入)”;
安全解析模式:
struct formatter<std::shared_ptr<MyStruct>, char> {
bool deref_ = false;
constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) {
auto it = ctx.begin();
auto end = ctx.end();
if (it != end && *it == 'd') {
if (++it != end && *it == 'e') {
if (++it != end && *it == 'r') {
if (++it != end && *it == 'e') {
if (++it != end && *it == 'f') {
deref_ = true;
++it; // 跳过 'f'
return it; // 返回 'f' 后位置
}
}
}
}
}
// 遇到任何不支持的字符,立刻返回 it(当前非法位)
return it;
}
// ...
};
- 永远别手写
*it >= '0' && *it 判断数字——用 <code>std::isdigit(*it),否则宽字符下失效 - 不支持的说明符不能静默跳过:遇到
'X'且你不支持,应抛std::format_error("unknown specifier 'X'") - parse() 必须是
constexpr,且返回类型必须与ctx.begin()一致(推荐用decltype(ctx.begin())而非硬写format_parse_context::iterator)
format() 中递归格式化字段必须用 format_to,禁用 std::format
复合对象如 struct Container { std::vector<:shared_ptr>> items; };</:shared_ptr>,若在 format() 里写 std::format("{}", item),会导致无限模板实例化:每次 std::format 都重新触发 std::formatter<shared_ptr>, char></shared_ptr> 查找,最终编译卡死或报 “template instantiation depth exceeds maximum”。
唯一安全路径是:
template <typename FormatContext>
auto format(const Container& c, FormatContext& ctx) const {
auto out = ctx.out();
*out++ = '[';
for (size_t i = 0; i < c.items.size(); ++i) {
if (i > 0) *out++ = ',';
// ✅ 正确:复用已有输出迭代器
out = std::format_to(out, "{}", *c.items[i]);
}
*out++ = ']';
return out;
}
-
std::format_to第二个参数是格式字符串,第三个起才是参数;它不新建 context,只往已有out写 - 不要用
std::string s = std::format(...);拼接再写入ctx.out()——性能差,且可能触发额外内存分配 - 若字段是私有成员(比如
MyStruct的m_x),format()无法访问,要么加friend struct std::formatter<mystruct char>;</mystruct>,要么提供公有 getter
智能指针空值、const 限定、char_type 成员缺一不可
漏掉 char_type 或 const 限定,GCC/Clang 可能报 “formatter does not satisfy basic_formatter”;MSVC 有时更宽容,但跨平台必须补全。
完整最小可行特化(含空值处理):
namespace std {
template<> struct formatter<std::unique_ptr<MyStruct>, char> {
using char_type = char;
constexpr auto parse(format_parse_context& ctx) { return ctx.end(); }
template <typename FormatContext>
auto format(const std::unique_ptr<MyStruct>& ptr, FormatContext& ctx) const {
if (!ptr) return format_to(ctx.out(), "null");
return format_to(ctx.out(), "UP[{}]", *ptr);
}
};
}
-
char_type = char是强制要求,不能靠推导;即使你只用char,也得显式声明 -
format()必须是const成员函数,否则 clang 会拒绝匹配 concept - 所有涉及输出的操作,必须通过
ctx.out(),不能用std::cout、std::string::append或临时std::ostringstream - 若复合对象含
std::optional<t></t>或std::variant,同样要为其单独特化std::formatter,且确保它们的头文件在 formatter 特化前已被包含
最常被忽略的是:把 formatter 特化写在 .cpp 文件里,或写在类定义之后但没保证所有使用点都 #include 了该头文件——这不会报错,但 std::format 就是找不到特化,然后回退到编译失败。


















