fmt
走进 fmt 内部:类型化入口、类型擦除与输出缓冲区
沿 fmt::format 追踪检查入口、vformat 与缓冲区,再看参数视图和动态存储怎样改变生命周期要求。
你将学会
- 沿一条真实调用链阅读
- 为什么检查类型后还要擦除类型
- 容易被忽视的边界是所有权
开始前需要
- 理解基础 C++ 值、引用、字符串与构建目标
- 能够区分文档预期与实际测量结果
追踪具体 fmt 行为,在输出、生命周期和验证边界明确的前提下选择集成方式。
先看结论
- 按固定版本追踪 format、vformat、缓冲区格式化和字符串转换。
- 类型擦除用于共享实现,不定义持久化的传输格式。
- 参数视图和动态存储的所有权语义依赖具体类型。
沿一条真实调用链阅读
format.h 中的 fmt::format 接收带类型参数包的 fmt::format_string,再把格式文字和 vargs 存储转交给 vformat。format-inl.h 中的 vformat 创建 memory_buffer,调用 detail::vformat_to,最后把累计缓冲区转换为 std::string。这样的路径能指向具体实现,而不是套用通用的输入、处理、输出图。
底层格式化路径对恰好只有两个字符的格式 {} 有专门分支,其他字符串则进入 parse_format_string 和携带解析、输出上下文的 format_handler。这是观察到的一个优化条件,不代表所有单字段格式都省去了解析;宽度、精度、附加文字和自定义格式都可能进入其他路径。
为什么检查类型后还要擦除类型
公开模板能看到参数类型,从而建立格式契约;之后使用类型擦除的实现,就可以在更多调用方之间共享处理逻辑,而不必针对每一种参数组合实例化完整实现。API 文档以小型日志包装器说明了这种模式,core.h 的 basic_format_args 则保存描述信息以及参数值或指针。
源码同时包含打包和未打包的参数表示,但这些实现细节不是稳定的应用序列化格式。不要持久化它们的内存布局,也不要把内部描述符当成跨版本协议。在应用边界上,更适合保留有语义的字段或已经完成的字符串,而不是库的私有实现对象。
容易被忽视的边界是所有权
fmt::format_args 是视图,make_format_args 使用左值引用来避免一部分临时对象生命周期问题,但这不等于视图拥有所有输入。如果格式化被延迟到参数存储或底层字符串已经失效之后,就需要额外设计所有权。立即格式化与异步日志排队是两个不同场景。
dynamic_format_arg_store 能为部分值提供存储:字符串和自定义类型可以复制进拥有所有权的节点,而字符串视图与显式引用包装器仍然借用其指向的数据。固定版本的 strings_and_refs 测试通过插入后修改字符数组,展示了这种差异。排队前应复制需要存活的数据,或者先完成格式化。
实施步骤
- 1
找到 format.h 中的公开模板。
- 2
跟进 format-inl.h 的 vformat 与精确 {} 分支。
- 3
阅读 core.h 的参数存储和生命周期说明。
- 4
分别测试字符串副本、字符串视图和显式引用。
可复制示例
{"entry":"fmt::format","sharedImplementation":"vformat","destination":"memory_buffer then std::string","exactFastPath":"{}","format_argsOwnsInputs":false,"dynamicStoreCopiesStringViewsBackingData":false}常见问题
fmt::format_args 能安全持有排队数据吗?
不能自动保证。它是视图,格式化操作期间,参数存储以及被借用的底层数据都必须保持有效。
使用动态存储就意味着所有参数都深拷贝了吗?
不是。所查实现区分拥有副本的字符串或自定义值,与借用底层数据的字符串视图、显式引用包装器。
资料来源
- doc/api.md来源核查 2026-09-08
- include/fmt/core.h来源核查 2026-09-08
- include/fmt/format.h来源核查 2026-09-08
- include/fmt/format-inl.h来源核查 2026-09-08
- include/fmt/args.h来源核查 2026-09-08
- test/args-test.cc来源核查 2026-09-08