每天一个开源项目#87 fmt:24.5K Stars 的 C++ 格式化核心

Trending #1 | 2026-09-03 | Stars 24,548 | Forks 2,996 | 主语言 C++ | License MIT | 仓库:github.com/fmtlib/fmt

C++ 里拼字符串,很多人第一反应还是 std::ostringstreamprintfstd::format。这三条路各有问题:流式 API 写起来啰嗦,printf 把类型安全交给运行时,标准库实现又常常受编译器和平台版本拖累。日志、错误消息、CLI 输出、序列化调试信息这些地方,看着都是小活,堆到一个大工程里就会变成编译时间、二进制体积和运行时开销。

fmt 做的事情很克制:把格式化这件事做快、做安全、做得足够好用。它不是最近才冒出来的新项目,2012 年就开源了,后来 C++20 std::format 的设计也受到它影响。今天它重新出现在 Trending 第一,更像是一个提醒:基础库不热闹,但基础库一旦站稳,影响的是成千上万行业务代码里最不起眼的那一层。

我看 fmt 最有意思的地方,不是 fmt::print("{}", value) 这种表层 API,而是它把三个矛盾压在一起处理:接近 printf 的输出效率、比 printf 强得多的类型检查、以及比很多模板库更低的编译和代码膨胀成本。这个平衡不容易。

📋 项目概览

项目 内容
项目名 fmtlib/fmt
一句话 面向 C++ 的高性能、类型安全格式化库,也是std::format 设计的重要来源之一
GitHub Stars 24,548(Trending 快照);API 稍后观测为 24,549
Forks 2,996
语言 C++ 95.7%,Python 2.0%,CMake 1.7%,C 0.4%
License MIT
版本 main 分支FMT_VERSION 120201,即 12.2.1;最新 GitHub Release 为 12.2.0
默认分支 main
最近提交 2026-09-02,Test dynamic width in nested_formatter
代码规模 浅克隆统计 145 个 tracked files,约 83,464 行文本文件;其中头文件约 42,606 行、.cc 约 27,217 行

🔥 为什么值得关注

C++ 的格式化问题很老,但它一直没有彻底消失。printf 快,可是类型不安全;iostream 类型安全,可是语法重、性能经常不好;std::format 更现代,但在老编译器、跨平台构建、编译时间控制上仍然有现实阻力。fmt 的位置正好卡在这些缝里:它给你一套稳定 API,又不要求项目立刻升级到最新标准库。

它的 README 把定位说得很直接:fast and safe alternative to C stdio and C++ iostreams。源码里能看到这句话不是只停在文档层面。include/fmt/base.h 里有编译期格式串检查相关的 format_string_checkerFMT_CONSTEVALFMT_ENFORCE_COMPILE_STRINGinclude/fmt/format.h 里有 Dragonbox 浮点到十进制转换路径;include/fmt/os.h 还提供了文件输出相关的缓冲封装。也就是说,fmt 并不是给 printf 包一层更漂亮的皮,而是把格式串解析、参数类型擦除、输出缓冲、浮点格式化都自己接管了。

我会把 fmt 看成 C++ 工程里的"低噪声基础设施"。它不会让架构图变漂亮,也不会制造很大的概念。但如果一个服务每天打印大量日志,或者一个 CLI 工具有大量用户可见输出,格式化库的成本就是真成本:CPU 时间、编译时间、可执行文件大小、错误暴露时机,都会被放大。

🏗️ 核心特性

  1. 类型安全的格式化 API

fmt::formatfmt::print 使用 {} 风格占位符,语法接近 Python 的 format,但绑定的是 C++ 类型系统。README 给出的最小例子很短:

cpp 复制代码
#include <fmt/base.h>

int main() {
  fmt::print("Hello, world!\n");
}

格式化字符串也可以直接返回 std::string

cpp 复制代码
std::string s = fmt::format("The answer is {}.", 42);
// s == "The answer is 42."

真正减少事故的是编译期检查。这个例子在 C++20 下可以在编译阶段暴露格式说明符和参数类型不匹配:

cpp 复制代码
std::string s = fmt::format("{:d}", "I am not a number");

源码侧的关键点在 include/fmt/base.hparse_format_string 负责扫描格式串,format_string_checker 负责把参数数量、命名参数和格式说明符约束接起来。运行时格式串并没有被禁止,但需要显式走 runtime_format_string 这条路径。这个设计很实用:默认安全,确实需要动态格式串时也留出口。

  1. 覆盖 C++20 std::format 与 C++23 std::print 的常见使用方式

fmt 的 API 形态和标准库格式化很接近。对工程迁移来说,这一点比"语法好看"更重要。你可以先在旧编译器或多平台项目里用 fmt,等工具链统一后,再评估是否切到标准库实现。

常见输出场景包括位置参数、时间格式化、容器输出、颜色样式:

cpp 复制代码
std::string s = fmt::format("I'd rather be {1} than {0}.", "right", "happy");
// s == "I'd rather be happy than right."
cpp 复制代码
#include <vector>
#include <fmt/ranges.h>

int main() {
  std::vector<int> v = {1, 2, 3};
  fmt::print("{}\n", v);
}

输出为:

text 复制代码
[1, 2, 3]
  1. Dragonbox 浮点格式化路径

浮点数转字符串不是简单的 sprintf 替代。正确舍入、最短表示、round-trip 保证会把实现复杂度拉高。fmt README 明确写到:IEEE 754 浮点格式化使用 Dragonbox 算法,目标是 correct rounding、shortness 和 round-trip guarantees。

源码里对应的路径集中在 include/fmt/format.h

text 复制代码
float/double 原始位模式
        │
        ▼
dragonbox::float_info<Float>
        │
        ▼
dragonbox::to_decimal(x)
        │
        ▼
decimal_fp{significand, exponent}
        │
        ▼
write_float / do_write_float
        │
        ▼
输出缓冲区

这条路径决定了 fmt 在大量数值输出场景里的价值。日志里打印 latency、坐标、金额、科学计算结果时,浮点转换的边界条件往往比整数更麻烦。fmt 把这些复杂性藏在库里,业务代码只需要关心格式语义。

  1. 缓冲输出与文件写入

include/fmt/base.h 里有内部 buffer<T>,它保存 ptr_size_capacity_,并通过 grow_fun 做扩容。这个抽象让格式化结果可以写到内存、迭代器或文件,而不是每格式化一段就立刻触发昂贵写入。

文件输出也有单独 API:

cpp 复制代码
#include <fmt/os.h>

int main() {
  auto out = fmt::output_file("guide.txt");
  out.print("Don't {}", "Panic");
}

README 给出的边界很清楚:这个路径在特定测试里最高可到 fprintf 的 9 倍,但这是文件缓冲场景,不应该被泛化成所有输出都固定快 9 倍。

  1. 编译时间和二进制体积控制

很多 C++ 模板库的问题不是运行慢,而是编译慢、膨胀大。fmt 在这点上做了专门对照。README 里的 format-benchmark 生成 100 个 translation units,每个调用格式化函数 5 次,模拟中等规模工程。

方法 优化构建编译时间 Executable size Stripped size
printf 1.6s 54 KiB 50 KiB
IOStreams 28.4s 98 KiB 84 KiB
fmt1122268 5.0s 54 KiB 50 KiB
tinyformat 32.6s 164 KiB 136 KiB
Boost Format 55.0s 530 KiB 317 KiB
方法 非优化构建编译时间 Executable size Stripped size
printf 1.4s 54 KiB 50 KiB
IOStreams 27.0s 88 KiB 68 KiB
fmt1122268 4.7s 87 KiB 84 KiB
tinyformat 28.1s 185 KiB 145 KiB
Boost Format 38.9s 678 KiB 381 KiB

这组数据的环境是 README 标注的 Apple clang 15.0.0、macOS Sonoma、best of three。它不能代表所有平台,但足够说明 fmt 的工程取舍:它没有为了模板表达力无限消耗编译器。

🔬 技术架构深度解析

fmt 的核心链路可以拆成四段:格式串解析、参数建模、格式化分发、输出写入。

text 复制代码
用户代码
  │
  ├─ fmt::format("{}", value)
  └─ fmt::print("{}", value)
        │
        ▼
格式串入口
  │
  ├─ 编译期字符串:format_string_checker
  └─ 运行时字符串:runtime_format_string
        │
        ▼
参数打包
  │
  ├─ make_format_args
  ├─ basic_format_arg
  └─ format_arg_store
        │
        ▼
formatter<T> 分发
  │
  ├─ 内置整数 / 字符串 / 浮点
  ├─ chrono / ranges / color 扩展头
  └─ 用户自定义 formatter 特化
        │
        ▼
输出层
  │
  ├─ memory_buffer
  ├─ iterator_buffer
  └─ output_file / buffered_file

格式串:先解析,再决定错误出现在哪里

格式串是 fmt 的第一道分界线。源码中 parse_format_string 扫描普通文本和 {} 替换字段;format_string_checker 接收参数包信息,然后检查每个字段的索引、命名参数、格式说明符是否能和参数类型对上。

这也是 fmt 比 printf 安全的核心原因。printf("%d", "abc") 这类错误可以一直拖到运行时,甚至变成未定义行为。fmt 的默认路径更倾向于把错误提前到编译期。代价是模板和 constexpr 逻辑更复杂,所以源码里也有不少编译器兼容判断,例如 Apple clang、MSVC 的 consteval 分支。

参数层:API 好看,内部不能全靠模板展开

如果每个格式化调用都把所有类型完整展开到最终输出路径,编译成本会很难控制。fmt 的做法是把参数包装成 basic_format_arg 一类的中间形态,再交给 basic_format_context 和对应 formatter<T>

这不是"零成本抽象"的口号式实现,而是比较务实的分层:外层 API 保留 C++ 类型推导的舒适度,内部用类型擦除和分发表降低重复实例化。对一个被大量业务文件 include 的库来说,这个取舍很关键。

输出层:小对象写入也要避免频繁系统调用

buffer<T> 的接口看上去普通,但它支撑了 fmt 的输出策略。内部保留当前大小和容量,空间不足时才调用 grow_iterator_buffer 可以把结果攒到本地小缓冲,满了再 flush 到外部迭代器;文件路径则通过 buffered_fileoutput_file 做更适合顺序写入的封装。

text 复制代码
formatter 写入字符
        │
        ▼
buffer<T>::append / try_reserve
        │
        ├─ 容量足够:移动指针,继续写
        └─ 容量不足:grow_ 扩容或 flush
                    │
                    ▼
             memory / iterator / file

这个层次解释了为什么 fmt 能同时服务 std::string、stdout、文件和自定义输出迭代器。格式化核心不需要关心最终落点,输出适配层负责把字符送出去。

性能数据要按场景看

README 的 speed test 使用 tinyformat_test.cpp,在 macOS 15.6.1、clang++ -O3 -DNDEBUG -DSPEED_TEST -DHAVE_FORMAT 下,把等价格式串填充 2,000,000 次并输出到 /dev/null。结果如下:

Library Method Run Time
libc printf 0.66s
libc++ std::ostream 1.63s
fmt 12.1 fmt::print 0.44s
Boost Format 1.88 boost::format 3.89s
Folly Format folly::format 1.28s

这组结果里 fmt 比 printf 快约 50%。我更愿意把它理解成"在这个格式串和这台机器上,fmt 没有为了安全付出性能税",而不是把它当成跨平台固定倍率。README 对 dtoa、文件输出、编译体积也给了单独基准,这种拆开讲的方式比一句"高性能"靠谱得多。

源码体量和模块分布

本次用浅克隆检查 main 分支,commit 为 bc82c408a6ac75d356396332b1e70793e86620ad。统计使用 git ls-files -z 写入文件后解析,避免终端输出截断影响数量。

指标 数值
tracked files 145
include/fmt 头文件 16
test 目录文件 67
文档文件 9
文本文件总行数 约 83,464
.h 行数 约 42,606
.cc 行数 约 27,217

从结构看,fmt 不是"大而全框架"。它的大头在头文件和测试。功能通过 base.hformat.hchrono.hranges.hcolor.hos.h 等头拆开,用户可以按场景引入;CMake 里也能看到 FMT_TESTFMT_DOCFMT_INSTALLFMT_FUZZFMT_OSFMT_MODULEFMT_UNICODE 等选项。

📖 README 核心内容摘要

README 的重点可以归成几类。

第一类是基础格式化。fmt::print 写 stdout,fmt::format 返回字符串,位置参数支持本地化时调整词序。

cpp 复制代码
std::string s = fmt::format("I'd rather be {1} than {0}.", "right", "happy");

第二类是现代 C++ 补齐。fmt 实现了 C++20 std::format 和 C++23 std::print 的常见能力,同时保留对较老编译器的可移植支持。工程里如果暂时不能统一 C++20 标准库,fmt 是更稳的落地路径。

第三类是扩展头。fmt/chrono.h 处理日期和时间,fmt/ranges.h 处理容器,fmt/color.h 处理终端颜色和样式,fmt/os.h 处理文件输出。

cpp 复制代码
#include <fmt/chrono.h>

int main() {
  auto now = std::chrono::system_clock::now();
  fmt::print("Time: {:%H:%M}\n", now);
}

第四类是可靠性。README 提到项目有大量测试,并接入 OSS-Fuzz。仓库浅克隆里 test/ 下有 67 个 tracked files;最近提交也仍然集中在 formatter 行为和测试上,比如 nested formatter 的动态宽度测试。

第五类是集成方式。fmt 支持 CMake 构建、安装目标、可选 header-only 配置,也能通过 FMT_HEADER_ONLY 宏走单头风格。这里要注意版本表述:main 分支当前宏版本是 12.2.1,而 GitHub Release 最新发布版是 12.2.0。生产项目通常应该优先跟 release tag 或包管理器版本,而不是直接追 main。

🚀 快速上手

下面这组命令已在本机用 AppleClang 21 和 CMake 4.4.3 跑过。CMake 选项来自仓库 CMakeLists.txt,并在构建中实际生效。

bash 复制代码
git clone https://github.com/fmtlib/fmt.git
cmake -S fmt -B fmt-build -DFMT_TEST=OFF -DFMT_DOC=OFF -DFMT_INSTALL=OFF
cmake --build fmt-build --target fmt -j2

最小程序:

cpp 复制代码
#include <fmt/base.h>
#include <fmt/format.h>

int main() {
  fmt::print("Hello, {}! {}\n", "fmt", fmt::format("{}", 42));
}

如果直接用源码编译一个单文件验证,可以这样做:

bash 复制代码
c++ -std=c++17 -Ifmt/include hello.cc fmt/src/format.cc -o hello
./hello

实测输出:

text 复制代码
Hello, fmt! 42

如果项目已经使用 CMake,更常见的方式是把 fmt 作为依赖并链接 fmt::fmt。README 还提到可通过 FMT_HEADER_ONLY 开启 header-only 模式,但我不建议在大工程里默认这么做:它方便集成,却可能把更多实现细节塞进每个编译单元。除非你的构建系统很难引入静态库或共享库,否则先用正常库目标更容易控制编译成本。

📊 增长速度与社区热度

这次 Trending 快照记录 fmt 为第 1 名,Stars 24,548,Forks 2,996。后续 GitHub API 观测到 Stars 为 24,549,只能说明两个采样点之间增加了 1 个 Star,不能把它写成完整日增。快照没有保留各仓库的今日新增 Star 数,所以这里标记为未保留。

按仓库创建时间粗算,fmt 从 2012-12-07 到 2026-09-03 约 5,019 天,24,548 Stars 对应长期平均约 4.9 Stars/天。这个数字只能作为长期基线。fmt 这种基础库的热度常常跟 release、标准库讨论、下游项目引用或社区文章有关,单日 Trending 排名并不等价于长期增长斜率。

社区维护信号比较健康:仓库未归档,最近 push 在 2026-09-02;API 记录 open issues 为 12;贡献者列表里 Victor Zverovich vitaut 为主要维护者,公开 API 返回贡献数 6,305,后面还有 phprusalexezederDanielaE 等贡献者。最近 GitHub Release 为 12.2.0,发布时间 2026-06-16;main 分支已推进到 12.2.1 开发状态。

完整 Trending 榜单如下。Stars 和语言列中,fmt 使用预抓取快照值;其他仓库为同日 GitHub API 补充观测。今日新增 Star 数没有被快照保留。

Rank Repository Language Stars 今日新增
1 fmtlib/fmt C++ 24,548 未保留
2 google-research/timesfm Python 30,306 未保留
3 DietrichGebert/ponytail JavaScript 122,374 未保留
4 debpalash/VoiceStudio Python 15,355 未保留
5 sngyai/Sequoia-X Python 6,346 未保留
6 ChromeDevTools/chrome-devtools-mcp TypeScript 50,762 未保留
7 NousResearch/hermes-agent Python 240,359 未保留
8 superlinked/sie Python 3,149 未保留
9 pacifio/atlas Rust 3,050 未保留
10 zyronon/TypeWords Vue 9,491 未保留
11 Imbad0202/academic-research-skills Python 45,744 未保留
12 affaan-m/ECC JavaScript 246,584 未保留
13 protocolbuffers/protobuf C++ 71,975 未保留
14 vercel-labs/portless TypeScript 11,937 未保留
15 blader/humanizer Python 40,809 未保留
16 JuliusBrussee/caveman Go 102,799 未保留
17 mattpocock/skills Shell 245,705 未保留
18 Gitlawb/openclaude TypeScript 32,119 未保留
19 firecrawl/pdf-inspector Rust 18,663 未保留

🎯 适用场景

场景 为什么适合 fmt 使用建议
服务端日志 大量字符串拼接和数值输出会放大格式化成本 优先用fmt::format 组装消息,日志库适配层再统一落盘
CLI 工具 输出格式多,用户可见文本需要可读性 用位置参数处理本地化词序,用fmt/color.h 控制终端样式
C++17/旧工具链项目 想要接近std::format 的 API,但标准库支持不齐 用 release tag 或包管理器固定版本,避免直接追 main
数值密集输出 浮点转字符串要兼顾正确性和速度 关注 Dragonbox 路径,同时用项目自己的数据做基准
大型 C++ 工程 编译时间和二进制体积同样重要 避免盲目 header-only,优先用库目标并关闭不需要的构建选项
文件批量输出 顺序写入需要减少系统调用和缓冲开销 评估fmt::output_file,但不要把 README 的 9 倍结果泛化到所有机器

💡 总结

fmt 的价值不在"格式化字符串更方便"这么浅。它把 C++ 里一个长期烦人的基础问题做成了成熟基础库:默认类型安全,API 接近标准库,浮点格式化路径认真,输出缓冲和编译成本也没有被忽略。

它也有边界。性能基准要看格式串、编译器、标准库、输出目标和硬件;main 分支版本和 release 版本不同;header-only 模式方便,但未必适合所有大工程。可这些边界反而说明 fmt 已经进入了工程现实,而不是停在 README 演示里。

如果你的 C++ 项目还在 printf、iostream 和手写拼接之间来回切,fmt 是一个值得认真评估的替代项。它不会改变业务架构,但会让很多低层输出代码少一点噪声,也少一点隐藏成本。

相关推荐
雪的季节2 小时前
Multisim14.0的详细中文安装步骤【附安装包】
c++
ovO2 小时前
DeepSeek Harness 源码解读(九):它适合什么场景,应该从哪里扩展
开源·agent
ovO2 小时前
DeepSeek Harness 源码解读(七):会话日志为何是唯一真相源
开源·agent
ovO2 小时前
DeepSeek Harness 源码解读(十):六条设计纪律如何约束可替换运行时
开源·agent
阿里云大数据AI技术2 小时前
PAI支持一键部署Qwen3.8-Flash-Next、GLM-5.3等最新开源模型
人工智能·开源·llm
ovO2 小时前
DeepSeek Harness 源码解读(八):文件、命令、审批与沙箱如何协作
开源·agent
Persistent的粽子!3 小时前
C++:类与对象(二)
开发语言·c++
ovO3 小时前
DeepSeek Harness 源码解读(六):Provider、Consumer 与能力接缝
开源·agent