Trending #1 | 2026-09-03 | Stars 24,548 | Forks 2,996 | 主语言 C++ | License MIT | 仓库:github.com/fmtlib/fmt
C++ 里拼字符串,很多人第一反应还是 std::ostringstream、printf、std::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_checker、FMT_CONSTEVAL、FMT_ENFORCE_COMPILE_STRING;include/fmt/format.h 里有 Dragonbox 浮点到十进制转换路径;include/fmt/os.h 还提供了文件输出相关的缓冲封装。也就是说,fmt 并不是给 printf 包一层更漂亮的皮,而是把格式串解析、参数类型擦除、输出缓冲、浮点格式化都自己接管了。
我会把 fmt 看成 C++ 工程里的"低噪声基础设施"。它不会让架构图变漂亮,也不会制造很大的概念。但如果一个服务每天打印大量日志,或者一个 CLI 工具有大量用户可见输出,格式化库的成本就是真成本:CPU 时间、编译时间、可执行文件大小、错误暴露时机,都会被放大。
🏗️ 核心特性
- 类型安全的格式化 API
fmt::format 和 fmt::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.h:parse_format_string 负责扫描格式串,format_string_checker 负责把参数数量、命名参数和格式说明符约束接起来。运行时格式串并没有被禁止,但需要显式走 runtime_format_string 这条路径。这个设计很实用:默认安全,确实需要动态格式串时也留出口。
- 覆盖 C++20
std::format与 C++23std::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]
- 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 把这些复杂性藏在库里,业务代码只需要关心格式语义。
- 缓冲输出与文件写入
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 倍。
- 编译时间和二进制体积控制
很多 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_file、output_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.h、format.h、chrono.h、ranges.h、color.h、os.h 等头拆开,用户可以按场景引入;CMake 里也能看到 FMT_TEST、FMT_DOC、FMT_INSTALL、FMT_FUZZ、FMT_OS、FMT_MODULE、FMT_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,后面还有 phprus、alexezeder、DanielaE 等贡献者。最近 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 是一个值得认真评估的替代项。它不会改变业务架构,但会让很多低层输出代码少一点噪声,也少一点隐藏成本。