【C++三方组件】fmt:C++字符串格式化的前世今生

【C++三方组件】fmt:C++字符串格式化的前世今生

【摘要】:printf 类型不安全,iostream 又慢又绕;fmt 用 {} 加变参模板给出答案,好到 C++20 收编为 std::format、C++23 收编为 std::print。How 实测基础与规格、具名参数、容器与 chrono、format_to、与 std::format 同源对照(逐字节一致)、四方案性能自测(fmt 最快)。Why 拆三问:凭什么比 printf 安全又快、进了标准为什么还要 fmt、v12 移除 fmt::localtime 说明什么。

【关键词】:fmt、格式化、std::format、类型安全、编译期检查

【版本基准】:{fmt} 12.2.0(MIT)|C++20(对照实验)|文中输出均为 g++ 13.1 实测

1. What:好到被标准收编

老问题:把变量拼进字符串,你只有两个都不称职的选项------printf 快但类型不安全(%d 配字符串,编译过、运行崩);iostream 类型安全但冗长、慢、还拖着 locale。{fmt} 的作者 Victor Zverovich 2012 年起给出第三个选项:{} 占位符 + 变参模板,安全、快、可扩展。

这个设计好到什么程度?标准委员会直接收编 :C++20 的 std::format 以 {fmt} 的语法为蓝本(作者本人参与提案 P0645),C++23 又收编了 std::print。于是今天的格局是:{fmt} 是先行者与超集,std::format 是标准化子集------本篇的一个实测亮点,就是同一格式串在两边跑出逐字节一致的输出。

2. 项目接入

json 复制代码
// vcpkg:vcpkg.json
{ "dependencies": [ "fmt" ] }
cmake 复制代码
find_package(fmt CONFIG REQUIRED)
target_link_libraries(app PRIVATE fmt::fmt)

还有一条零构建的路:{fmt} 支持纯头文件模式#define FMT_HEADER_ONLY 1 后只需 include 路径,不链任何库(本篇全部示例即此模式)。快速原型与教学场景,这是最快的路径。

3. 核心概念:格式字符串是编译期接口

fmt 的世界观一句话:格式字符串不是运行期数据,是编译期接口fmt::print("{:>8.2f}", x) 里的规格在编译期被解析校验------类型对不上、规格写错,直接编译错误,而不是运行期的 UB 或崩溃。三个支柱:

支柱 含义
{} 占位 + 规格语法 {0} 位置、{:>10} 对齐宽度、{:.2f} 精度
变参模板转发 类型信息全程保留------printf 的 % 猜类型消失
formatter<T> 特化 任何自定义类型都能接入这套语法(可扩展性)

对照一下三种时代的写法,气质立现:

cpp 复制代码
printf("%s v%d.%d\n", name, major, minor);  // 靠人肉对表
std::cout << name << " v" << major << ".";  // 串珠子
fmt::print("{} v{}.{}\n", name, major, minor);  // 声明式

4. How:六组 API(实测)

基础与规格------对齐、宽度、精度、符号补零、动态宽度一应俱全:

cpp 复制代码
fmt::print("{0} {1} {0}\n", "abra", "cadabra");
fmt::print("[{:>10}]\n", "right");    // 右对齐宽 10
fmt::print("[{:.2f}]\n", 3.14159);    // 精度
fmt::print("[{:+08.3f}]\n", 42.5);    // 符号+补零
fmt::print("[{:{}}]\n", "dyn", 8);    // 动态宽度

实测输出:

text 复制代码
abra cadabra abra
[     right]
[3.14]
[+042.500]
[dyn     ]

具名参数------配置化日志的救星:

cpp 复制代码
fmt::print("host={host} port={port}\n",
           fmt::arg("host", "127.0.0.1"),
           fmt::arg("port", 8080));

扩展头 ------容器与时间直接格式化(fmt/ranges.hfmt/chrono.h):

cpp 复制代码
std::vector<int> v{1, 2, 3};
std::map<std::string, int> m{{"a", 1}, {"b", 2}};
fmt::print("vec={} map={}\n", v, m);
using namespace std::chrono_literals;
fmt::print("dur={}\n", 9'876'543ms);
// 实测:vec=[1, 2, 3] map={"a": 1, "b": 2}
//       dur=9876543ms

自定义类型 ------formatter<T> 特化把自家类型接进同一套语法:

cpp 复制代码
struct Point { int x, y; };

template <> struct fmt::formatter<Point> {
  constexpr auto parse(fmt::format_parse_context& ctx) {
    return ctx.begin();    // 不消费任何规格
  }
  auto format(const Point& p,
              fmt::format_context& ctx) const {
    return fmt::format_to(ctx.out(),
                          "({}, {})", p.x, p.y);
  }
};

fmt::print("{}\n", Point{3, 4});   // 实测:(3, 4)
// fmt::print("{:>12}\n", Point{3, 4});
// ❌ 编译错误:parse 未声明接受规格------
//    编译期校验连自定义类型都管(实测踩到)

format_to 家族------写入调用者的缓冲,跳过 std::string 分配:

cpp 复制代码
char buf[64];
auto it = fmt::format_to(buf, "{}!", "fmt");
*it = '\0';                          // 返回结束位置
auto r = fmt::format_to_n(buf, 5, "{}", 1234567);
// r.size = 7:被截断也会告知完整长度

与 std::format 同源对照(本篇的核心实验):

cpp 复制代码
std::string a = fmt::format("{:>8.2f}%", 97.5);
std::string b = std::format("{:>8.2f}%", 97.5);
// 实测:fmt=   97.50% std=   97.50% same=1

性能自测 (20 万次 "i={} x={:.3f}",g++ 13.1 -O2):

方案 耗时 相对
fmt::format 32.4 ms 1.0×
std::format 45.8 ms 1.4×
snprintf 69.1 ms 2.1×
ostringstream 224.5 ms 6.9×

fmt 快于 snprintf 本身就是反直觉的实据;对 ostringstream 是数量级优势。

5. Why:三个追问

① 凭什么比 printf 又安全又快? 安全来自编译期:类型由模板推导,规格由 consteval 解析(C++20 下 fmt::print("{:d}", "str") 直接编译失败);快来自三件事------默认与 locale 无关 (printf/iostream 的小数点、千分位随全局 locale 漂移,fmt 的输出跨机器恒定,这本身就是确定性哲学的一部分)、浮点用 Dragonbox 最短表示算法(官方口径快于 printf 家族,12.2 起默认开全量查找表再提速 10~25%)、输出直写缓冲。安全与快不矛盾,都来自「把工作前移到编译期、把不确定性挡在运行期外」

② 进了标准为什么还要 fmt? 因为标准收编的是子集 :颜色输出(fmt/color.h)、printf 兼容层、文件输出(fmt/os.h)、更宽的编译器矩阵(C++11 起步,std::format 要 C++20,GCC 13 / MSVC 2019 19.29 才齐)都还在 fmt 这边。决策很简单:项目能上 C++20 且只需要基础能力,用 std::format;其余情况(C++17、要颜色/兼容层/极致性能)用 fmt------两者语法同源,迁移近乎零成本。

③ v12 移除 fmt::localtime 说明什么? 本篇实测亲身踩到:按旧教程写 fmt::localtime(now),v12 直接编译错误------时区相关 API 已移除,推荐 std::localtime。教训有两条:fmt 的版本迁移成本真实存在(破坏性变更会清退旧 API,跟随 ChangeLog 升级);「标准库有的,库就让位」是它的一贯哲学------这也是先行者与标准共存的长期姿态。

6. 坑与最佳实践(实测依据)

  1. 版本敏感 :跨大版本升级先读 ChangeLog.md(v12 连核心头都从 core.h 改成了 base.h)。
  2. C++17 下的编译期检查 要显式 FMT_STRING("...");C++20 起字面量自动检查,别再裹一层。
  3. 热路径用 format_to + 复用缓冲 (如 fmt::memory_buffer),跳过字符串分配。
  4. 自定义类型fmt::formatter<T> 特化,别重载运算符老三样。
  5. header-only 与编译库二选一:全工程统一,混用会双份符号。
  6. 编码立场 :fmt 把字符串按字节透传,UTF-8 内容原样输出、无需转码;宽字符走 wchar_t 重载。跨编码环境的坑在下一篇 utfcpp 展开。

7. 选型对比

{fmt} std::format(C++20+) printf 家族 iostream
类型安全 ✅ 编译期 ✅ 编译期
性能 最快(自测) 次之 最慢
标准/依赖 三方(MIT) 标准库 标准库 标准库
特性 颜色/chrono/ranges 超集 基础

一句话:有 fmt 可用就用 fmt,标准锁定 C++20+ 且要求零依赖时用 std::format;printf 留给 C 接口边界,iostream 建议功成身退。

8. 延伸与联动

  • 官方文档 fmt.dev------API 与语法权威,ChangeLog.md 是升级必读;
  • cppreference 的 std::format 页面可直接对照本篇 §4 的同源实验;
  • 文本处理部分继续:下一篇 RE2------「性能可预测」在正则世界的化身,一个把安全做成核心卖点的引擎。〔关联 第 11 篇〕

参考fmtlib/fmt 12.2.0(MIT)。文中格式化输出、std::format 对照(same=1)、性能自测(g++ 13.1 -O2,20 万次)均为本机实测;Dragonbox 提速口径引自官方 ChangeLog。

相关推荐
anew___1 小时前
舵机——让 Arduino 动起来
c++·stm32·单片机·嵌入式硬件·c
qq_344920561 小时前
Qt 鼠标双击QLabel全屏显示图像
c++·qt
f狐0狸x2 小时前
【C++修炼之路】C++继承的探索
c++·继承
虚无的纽扣2 小时前
【C++】C++11的魔法:不止右值引用,C++11赋予的编程神力
c++
码匠许师傅2 小时前
【C++三方组件】RE2:工业级正则的安全与性能
c++
ThornArmor3 小时前
《向内深潜,向外飞掠》
c语言·开发语言·c++·vim·visual studio
j7~3 小时前
【C++微服务项目开发脚手架】(环境篇)虚拟机 + Docker + MySQL/Redis/RabbitMQ/ES/etcd/FastDFS 全套配齐
linux·c++·ubuntu·docker·vmware·项目开发·微服务脚手架
qq_199886873 小时前
第5板块·第4节:性能优化中的高级技巧
c++·人工智能·gpu算力
郝学胜_神的一滴3 小时前
C++20模板元编程 04:变量模板 别名模板与模板Lambda实战
c++·visual studio