C++ std::print
C++23 引入了 std::print 和 std::println。它们建立在 C++20 std::format 之上,用类型安全的 {} 格式串替代了 printf 的 % 风格格式串。
std::print 不追加换行,std::println 会在末尾追加一个换行符;二者都默认写入 C 标准输出 stdout,而不是 std::cout。换行不等于刷新缓冲区,需要立即刷新时仍应显式处理。
使用前提
需要同时满足以下条件:
- 使用 C++23 或更新标准,例如
c++ -std=c++23 main.cpp; - 编译器所搭配的标准库实现了
<print>;仅打开 C++23 开关并不保证该功能可用。
可用特性测试宏进行检查:
1 | |
基本用法
1 | |
在大多数日志和报表场景中,std::println 更简洁;需要分段输出、进度条或自行控制换行时再使用 std::print。
输出到其他目标
除默认输出外,还可以传入 C 的 FILE* 或 C++ 的 std::ostream:
1 | |
前者适合与 printf/fprintf 代码互操作,后者适合既有的流接口。两种形式都不改变格式串语法。
格式串规则
以下以 C++23 为基线。std::print、std::println 与 std::format 使用相同的格式串规则,但不同参数类型拥有各自的格式说明语法,不能把数值格式说明直接套到 range 或时间类型上。
替换字段、索引与大括号
格式串由普通字符、替换字段和转义大括号组成。替换字段的形式为 {arg-id} 或 {arg-id:format-spec};索引和格式说明都可以省略,{} 与 {:} 都使用默认格式。
1 | |
- 索引从
0开始,只能是十进制整数;标准接口不支持{name}这样的命名参数,也不支持{0.member}或{0[1]}这样的表达式。 {}按出现顺序使用自动索引;{0}、{1}使用手动索引,可以重排、重复引用或跳过参数。- 同一格式串不能混用自动索引与手动索引,动态宽度、精度中的索引也参与这项限制。
- 字面量大括号必须写成
{{、}};反斜杠不能转义格式串的大括号。\n、\t则是 C++ 字符串字面量的转义,先由语言处理。 - 参数可以多于实际使用的数量,但引用不存在的参数无效。
std::println("{}", 1, 2)合法,只输出1。
类型说明符前必须有冒号:{:x} 是十六进制格式,{x} 不是合法索引,也不是十六进制格式。
标准格式说明的完整顺序
基本算术类型、指针和字符串使用以下语法。方括号表示可选部分,不是要写进格式串的字符:
1 | |
| 部分 | 可用写法 | 含义与限制 |
|---|---|---|
fill | 单个填充字符,默认空格 | 必须紧邻并位于 align 前;不能是 { 或 } |
align | <、>、^ | 左对齐、右对齐、居中 |
sign | +、-、空格 | 正负都显示符号、只显示负号、非负值前放空格 |
# | # | 替代形式:整数进制前缀、浮点小数点及部分尾随零 |
0 | 0 | 数值零填充,放在符号及进制前缀之后 |
width | 正十进制整数或 {}、{索引} | 最小显示宽度,不截断内容 |
.precision | .非负整数、.{}、.{索引} | 仅浮点数、字符串支持;语义取决于类型 |
L | L | 算术类型的 locale 格式化,详见本地化一节 |
type | 类型说明符 | 必须与实参类型兼容,详见下表及各类型小节 |
顺序固定。例如 {:+#08x} 合法,{:8+x} 不合法。所有选项可省略,但并不是每种类型都支持所有选项。
| 实参类型 | 默认形式 | C++23 支持的类型说明符 |
|---|---|---|
整数,不含 bool 和格式字符类型 | 十进制 | b B c d o x X |
char,用于窄字符格式串 | 单个字符 | c ? b B d o x X |
bool | true / false | s b B d o x X |
| 浮点数 | 可恢复原值的最短表示 | a A e E f F g G |
| 字符串、字符串视图、C 字符串 | 原文 | s ? |
void*、const void*、nullptr | 0x 前缀的地址 | p |
这里的类型说明符不是转换操作。整数不能用 f,浮点数不能用 d;需要改变类型时应先显式转换。std::print 的格式串是窄字符形式,不能因此直接输出 std::wstring、char8_t 字符串等不同字符类型的文本。
对齐、填充与显示宽度
宽度是最小显示宽度,包含符号、进制前缀等最终输出内容。较长内容原样保留。字符串、字符及文本形式的 bool 默认左对齐;数值及指针默认右对齐。字符与 bool 使用整数说明符时也默认右对齐。
1 | |
居中时若填充数为奇数,右侧比左侧多一个填充字符。fill 在 Unicode 编码下可以是单个 Unicode 标量值,但库计算填充数时按每个填充字符宽度为 1 处理;用双列汉字或 emoji 填充可能造成视觉错位。
文本宽度不是字节数,也不简单等于 std::string::size()。Unicode 文本按显示宽度估计,标准建议以扩展字素簇及其首码点估算,部分宽字符计为两列。实际终端的 emoji、组合字符、ANSI 控制序列显示效果可能不同,表格排版仍应在目标终端验证。
符号、替代形式与零填充
sign 的默认值为 -。+ 为非负值加正号,空格为非负值留一列符号位置;负数始终保留负号,浮点负零也保留负号。这些规则同样适用于浮点无穷大和 NaN。
1 | |
0 是数值选项,区别于 0> 中的普通填充字符:前者保留符号和前缀的位置,后者只在整段输出前填充。显式指定对齐后,0 选项不再产生零填充;无穷大与 NaN 也不做数值零填充。
sign、#、0 用于数值;字符和 bool 只有选择整数说明符后才能使用这些选项。字符串不支持它们。以字符形式输出的 c 也不能带数值符号、进制前缀或数值零填充。C++23 的指针不支持 0 选项和大写 P,这两项属于 C++26 的增强,不能因某个库提前支持就视为 C++23 的可移植写法。
整数与进制
1 | |
# 对十进制没有额外前缀;八进制只在非零值前补 0,因此零不会输出为 00。负整数仍以负号加数值的方式输出,不会自动变成补码位模式;若要观察位模式,应先转换为所需宽度的无符号类型。
整数不支持精度,{:.4d} 无效;补足四位应使用 {:04d}。c 把整数转换成单个格式字符,值超出字符类型的可表示范围会抛出 std::format_error,并不把 Unicode 码点编码成 UTF-8。
浮点数与精度
| 类型 | 表示方式 | 精度含义与省略精度的行为 |
|---|---|---|
| 无类型 | 紧凑表示,可选定点或科学计数法 | 有精度时为有效数字;无精度时选择能恢复原值的最短表示 |
f / F | 定点 | 小数点后位数,默认 6 |
e / E | 科学计数法 | 小数点后位数,默认 6;E 使用大写指数标记 |
g / G | 按数值和精度选择定点或科学计数法 | 有效数字位数,默认 6;精度 0 按 1 处理 |
a / A | 十六进制浮点 | 小数点后十六进制位数;无精度时选择能恢复原值的最短表示 |
{} 不等于 {:g}:前者默认最短表示,后者默认六位有效数字。a / A 不带 0x / 0X 前缀;如 42.0 用 {:a} 输出 1.5p+5。
1 | |
精度控制转换时的舍入,不是简单裁剪字符串。# 让有限浮点值即使没有小数位也保留小数点;对 g / G 还保留尾随零。小写说明符输出 inf、nan,大写说明符输出 INF、NAN。
动态宽度与动态精度
宽度、精度可以引用其他参数,但不能写表达式,也不能继续嵌套格式说明:
1 | |
第一个例子依次使用参数 0 作为值、1 作为宽度、2 作为精度。手动索引的版本必须把所有索引都写全;{0:>{}} 属于混用索引,无效。
动态宽度和精度必须来自标准有符号或无符号整数类型,例如 int、unsigned、std::size_t;bool、char 和浮点数不符合要求。动态值允许为 0,不能为负;负值会抛出 std::format_error。写死的宽度必须是正整数,.0 则是合法精度。
字面量格式串的语法和类型可以在编译期检查,但动态数值仍在运行时检查。例如格式串合法的 std::println("{:{}}", 42, width),若 width 为负,仍然会失败。
字符串、字符与调试输出
字符串支持默认形式、s 和 C++23 的调试说明符 ?。精度限制最终格式化文本的最长显示前缀,不是字节上限;与宽度组合时,先限制前缀,再补齐宽度。使用 ? 时限制的是转义后的表示,包括引号和转义字符,截断结果不保证仍是完整的引号表达式。
1 | |
? 为字符串加双引号,为字符加单引号;对换行、制表、回车、引号、反斜杠等生成转义,对其他部分不可显示字符使用 \u{...},对无效编码单元使用 \x{...}。它不是 JSON 序列化器,不应拿调试输出作为交换协议。bool 不支持 ?,单个字符也不支持精度。
const char* 默认按以 '\0' 结尾的 C 字符串处理,不按地址输出;空指针不能作为有效 C 字符串。需要保留内嵌零字符时,传入带长度的 std::string 或 std::string_view。
指针与地址
标准地址格式化直接支持 void*、const void* 和 nullptr;其他对象指针需要显式转换。字符指针要看地址时也必须转换,不能仅写 {:p} 来改变其字符串语义。
1 | |
地址值依赖运行环境;{:p} 使用 0x 前缀和小写十六进制,支持普通填充、对齐和宽度,不支持精度、sign、# 或 L。函数指针、成员指针不属于这类可移植的对象地址格式化。C++23 也不能假定智能指针本身可格式化,应按需要取 .get() 后转换。
常见无效格式
| 格式或用法 | 无效原因 | 正确表达 |
|---|---|---|
"{} {0}" | 自动、手动索引混用 | "{} {}" 或 "{0} {1}" |
"{0:>{}}" | 内外字段的索引模式混用 | "{0:>{1}}" |
"{name}"、"{x}" | 标准接口不支持命名索引 | "{}"、"{:x}" |
整数用 "{:.2}"、"{:f}" | 不支持精度或类型不匹配 | 零填充,或先转成浮点数 |
字符串用 "{:+}"、"{:08}" | 数值专属选项 | "{:>8}"、"{:0>8}" |
"{:8+x}" | 选项顺序错误 | "{:+8x}" |
"{:>{:d}}" | 动态宽度字段不能带格式说明 | "{:>{}}" |
range 用 "{:.2}"、"{:x}" | 外层不支持精度或整数类型 | "{::.2f}"、"{::x}",且元素类型匹配 |
字面量中的这些错误通常导致编译失败;通过 std::vformat 使用运行时格式串时,则会抛出 std::format_error。自定义类型支持哪些格式说明,由其 std::formatter::parse 决定,不能假定所有标准选项都自动可用。
运行时格式串
std::print 的格式参数是 std::format_string。因此,字面量格式串会在编译期校验参数数量、索引和类型;运行时取得的 std::string_view 不能直接作为它的格式串。
1 | |
运行时格式串无效时,std::vformat 会抛出 std::format_error。不要把不受信任的输入直接当作格式串;若只是输出用户提供的文本,应写成 std::println("{}", text)。
C++23 的 range 输出规则
range 格式化由 <format> 提供,std::print 复用这些格式化器;使用容器、视图时还要包含对应头文件。除了 __cpp_lib_print,还应确认 __cpp_lib_format_ranges >= 202207L:库支持 <print> 不代表一定实现了 range 格式化。
支持范围与默认形式
支持的核心条件是 range 满足 std::ranges::input_range,且实际迭代引用类型可格式化。容器、C 数组、std::span、许多 ranges 视图都可以输出,嵌套 range 则递归格式化。字符数组、std::string、std::string_view 等已有字符串格式化器,不会因为也是 range 就默认变成字符列表。
| 类型或类别 | 默认输出示意 | 说明 |
|---|---|---|
顺序 range:vector、array、list、span、普通视图等 | [1, 2, 3] | 方括号,元素之间 , |
集合:set、multiset、unordered_set 等 | {1, 2, 3} | 大括号,元素之间 , |
映射:map、multimap、unordered_map 等 | {"a": 1, "b": 2} | 大括号,键值之间 :,条目之间 , |
字符 range:vector<char>、array<char, N> 等 | ['a', 'b'] | 默认逐字符调试输出,不自动拼成字符串 |
字符串 range:vector<string> 等 | ["a", "b"] | 字符串元素默认带引号及转义 |
| pair / tuple 的 range | [(1, 2), (3, 4)] | 元素默认圆括号,不自动当成 map |
| 嵌套顺序 range | [[1, 2], [3, 4]] | 每层保留自己的格式 |
| 空 range | [] 或 {} | 只输出对应的开闭括号,没有分隔符 |
1 | |
输出顺序就是迭代顺序:有序关联容器按比较器顺序,unordered 容器没有可依赖的固定顺序,不应把输出顺序写死在快照或协议里。
默认分类由 std::format_kind<R> 决定,不仅取决于元素形状:有 key_type 的类型通常按集合处理,同时具有 mapped_type 且元素为 pair 或二元素 tuple 时按映射处理,其余通常为顺序 range。因此 vector<pair<...>> 默认仍是列表;关联容器经普通视图包装后,也可能变成列表,因为视图没有原容器的分类信息。
对于程序定义的 range 类型,可以特化 std::format_kind 选择 sequence、set、map、string、debug_string 或 disabled,但必须满足对应分类的约束;不能为纯标准库类型随意改变分类。选择 string / debug_string 的自定义 range 使用字符串格式说明,而不是下面的通用 range 格式说明。
range 格式说明的完整语法
1 | |
| 部分 | 写法 | 作用 |
|---|---|---|
| 外层填充、对齐、宽度 | *^20、>20、>{} | 对整段 range 输出生效,默认左对齐 |
n | n | 去掉当前层的开闭括号,保留分隔符 |
range-type | m、s、?s | map 形式、字符串形式、转义字符串形式 |
element-spec | 再写一个 : 后接元素格式 | 给每个元素使用同一份格式说明 |
外层没有 sign、#、数值零填充、精度或 L;这些选项若适用于元素,应写进 element-spec。range 的填充字符还不能是 :,以免与元素格式的起始冒号冲突。选项顺序固定,去括号的 map 形式写 nm,不是 mn。
第一个冒号引出 range 格式说明,第二个冒号才引出元素格式说明。{:>12} 对齐整个 range,{::>4} 对齐每个元素。外层宽度包含括号、分隔符及元素格式化后的内容,不是元素数量上限。
1 | |
{:n} 只去掉当前层括号,不是 join 操作,不会去掉逗号和空格。空 range 使用 {:n} 得到空文本;若同时有宽度,仍会产生填充。
字符与字符串元素的默认调试格式
未显式写 element-spec 时,range 会为支持 set_debug_format() 的元素格式化器启用调试格式;标准字符串和字符格式化器因此带引号及转义。显式写第二个冒号,即使后面为空,也会取消该层自动启用的调试格式:
1 | |
指定字符串元素宽度或精度而仍想保留调试形式时,要显式加 ?,例如 {::>8?}。这条规则不意味着 tuple 的字段也能关闭调试格式:tuple 有自己的格式化规则,详见下文。
s 与 ?s:把字符 range 当作字符串
s 和 ?s 只适用于元素类型与格式串字符类型相同的 range;对于 std::print,就是 char range。vector<int>、vector<unsigned char>、字符串的 range 都不能这样使用。
1 | |
s 按 range 的全部字符拼接,不以零字符提前结束;?s 再为整段字符生成带双引号的转义表示。{:s} 的内嵌换行与 std::println 追加的换行都有效,示例会产生两个换行。
两种形式都能指定外层填充、对齐、宽度,但不能与 n 或元素格式组合,也不支持外层精度。{:ns}、{:s:}、{:.3s} 都无效。要限制输出字符范围,应先生成有限视图或带长度的字符串视图。
m:以键值对形式输出
m 要求每个元素是 std::pair 或恰好含两个字段的 std::tuple。它把外层括号改为 {},把每个元素改为 key: value,条目之间仍为 ,。它只改变显示,不会排序、合并重复键或构造关联容器。
1 | |
注意 {:m} 是 range 层的 map 形式,{::m} 是 元素 tuple 层的 map 条目形式;后者不会改变外层方括号。显式的 range m 要求条目按 tuple m 形式输出,不能借元素 n 把它改回逗号布局:{:m:n} 仍输出 {1: 2, 3: 4}。需要独立控制两层时,直接使用 {::m}、{::n} 等没有外层 m 的形式更清晰。
标准 map 默认已有 map 形式,不必再加 m;{:n} 可只去掉外层大括号。tuple 格式说明不能继续把 x、.2f 等分别传给键和值,不能写 {::x} 来让一整个 pair 变成十六进制。
pair / tuple 自身的规则
pair 和 tuple 不是靠 range 迭代来格式化的,但经常作为 range 元素出现。它们使用独立语法:
1 | |
tuple-type 只能省略、写 n 或写 m,二者不能组合。默认以 (a, b, ...) 输出;n 去圆括号但保留 ,;m 仅适用于两个字段,去圆括号并把分隔符换成 :。外层宽度、对齐作用于整个 tuple,字段字符串、字符默认调试输出。
1 | |
tuple 格式不支持额外的元素格式冒号,不能给各字段设置独立精度或类型。需要控制字段时,应把字段拆成独立替换参数,或使用自己的包装类型及格式化器。
嵌套 range 与动态参数
每下降一层 range,就多经过一个元素格式冒号。嵌套括号不会被外层 n 自动去掉:
1 | |
这里 {:::02} 中第一个冒号引出外层格式,第二个冒号引出内层 range 格式,第三个冒号引出整数元素格式。去掉两层括号只是让显示变平,不改变数据结构或分隔符。
1 | |
动态宽度、精度仍遵循全局索引规则。元素格式只解析一次,因此同一个动态参数会应用于所有元素,并不是每个元素再消费一份参数。
视图、适配器与使用边界
1 | |
- 视图不必先物化成容器;格式化时会实际遍历并计算元素。非
const可迭代的视图应保持为可变对象,不能一律写成const auto。 - 单遍 input range 的输出可能消耗输入;有副作用的视图也会执行对应操作,不应把打印视为完全无影响的观察。
- 无界 range 没有“默认只打印前几项”的规则,可能永不结束。日志中应先用
std::views::take(n)限制元素数;外层宽度不能代替这个限制。 - C++23 另为
std::queue、std::stack、std::priority_queue提供格式化器,按底层容器顺序输出。stack 不保证以弹栈顺序输出,priority queue 输出堆布局,不是不断pop()得到的排序序列;格式化本身不做pop()。 - 若元素本身不可格式化,range 也不能格式化。例如 C++23 没有通用的
optional、variant格式化器,仅套一层vector不会使其可输出。 - 元素引用仍是自身 range 类型的递归结构会被默认 range 分类禁用,以避免约束递归,例如
std::filesystem::path。不能假定凡有begin/end就能这样打印。
标准 range 格式没有任意分隔符或括号的格式串选项;n 只能删除括号,不能把 , 换成 ;。简单自定义输出可自行循环;需要复用格式串规则时,为程序定义的包装类型特化 std::formatter,复用 std::range_formatter 的 set_separator、set_brackets、underlying()。不要为 std::vector<int> 等纯标准库类型新增格式化器特化,也不要把 {fmt} 的 fmt::join 当成标准接口。
时间与本地化格式化
时间
chrono 类型的格式说明使用独立语法,% 转换符仅在 chrono 字段内部有特殊意义,不能与 printf 的数值格式混为一谈:
1 | |
| 转换符 | 含义 |
|---|---|
%Y、%y、%C | 年、两位年、世纪 |
%m、%b / %h、%B | 数字月、月份缩写、月份全名 |
%d、%e | 月内日,分别用零、空格补齐 |
%F、%D | %Y-%m-%d、%m/%d/%y |
%a、%A、%u、%w | 星期缩写、全名、周一为 1 的数字、周日为 0 的数字 |
%j | 年内日;对 duration 则表示经过的天数 |
%U、%W | 以周日、周一为每周起点的年内周数,首个起点前为第 00 周 |
%G、%g、%V | ISO 周历年、两位 ISO 周历年、ISO 周数;周历年不一定等于日历年 |
%H、%I、%M、%S | 24 小时制小时、12 小时制小时、分、秒 |
%R、%T | %H:%M、%H:%M:%S |
%p、%r | AM/PM、本地化的 12 小时制时间 |
%c、%x、%X | 本地化的日期时间、日期、时间 |
%Q、%q | duration 的数值、单位后缀 |
%z、%Ez / %Oz、%Z | UTC 偏移 +hhmm、带冒号偏移 +hh:mm、时区缩写 |
%%、%n、%t | 字面量 %、换行、制表符 |
部分转换符允许 E 或 O 修饰,分别选择 locale 的替代历法表示或替代数字表示,不是所有转换符都能任意加修饰;完整合法组合见参考中的 time.format。不带 L 时采用 "C" locale,带 L 时才使用传入或全局 locale。
chrono 字段必须包含转换符需要的信息:duration 可以输出经过的时分秒,但不能输出星期或日期;缺少信息会导致格式错误。只有浮点表示的 std::chrono::duration 支持 .precision,不能用 {:.2%T} 任意截断时间点;时间点的秒小数位通常取决于其 duration 精度,应先转换或 floor 到目标时间单位。
1 | |
不指定 chrono-specs 时使用类型默认的文本形式。<chrono> 类型也可以直接指定日期时间布局:
1 | |
这里格式化的是 system_clock 时间点本身,按 UTC 展示;若需要按特定时区展示,应先构造对应时区的 zoned_time。sys_time 可输出 UTC 的 %z 和 %Z,但一般的 local_time 没有时区信息,不能凭它生成偏移量或时区名称。
本地化
L 选项使用格式化上下文的 locale。std::print 本身没有 locale 参数,可先用 std::format 生成字符串:
1 | |
分组符号、十进制符号和可用 locale 名称均依赖平台;示例不应假设所有机器都输出 1,000,000。
L 不会自动本地化所有字段:对整数控制数字分组,对浮点数控制分组和小数点,对文本形式的 bool 使用 locale 的真/假名称。不带 L 的基本数值输出不受全局 locale 影响;带 L 但未显式传 locale 时使用全局 locale,stdout 或目标流的 locale 不能代替格式化上下文的 locale。
range 外层不能写 L,可通过 {::L} 或 {::.2Lf} 本地化各元素;pair / tuple 没有把 L 传给各字段的格式串语法,需拆开字段格式化。
自定义类型
为自定义类型特化 std::formatter 后,它就能被 std::print、std::format 等所有格式化接口复用。下面的格式化器只支持默认的 {} 形式:
1 | |
只能为程序定义的类型定制 std::formatter。若格式化器需要支持对齐、精度或自定义选项,应在 parse 中显式解析,并在 format 中贯彻这些规则。
性能、错误与并发
std::print 的价值首先是可读性和类型安全;标准并不承诺它在所有平台、所有格式和所有输出目标上都快于 printf。格式化开销、标准库实现、缓冲策略以及终端或磁盘 I/O 都会影响结果,性能敏感路径应以目标环境的基准测试为准。
一次输出的格式化失败或底层写入失败仍可能报错,因此关键路径不能把输出当作永远成功的操作。还应注意:
std::print和std::println返回void,不能像printf一样取得写入字符数;std::println只追加\n,不保证刷新缓冲区;- 并发日志中不要用多次调用拼接一条记录,否则可能与其他线程的输出交错。尽量将一条记录放进一次
std::println;需要跨多步保持一致时,使用互斥锁或std::osyncstream。
没有 std::print 时使用 libfmt
标准库尚未实现 <print> 时,可以使用 {fmt} 的 fmt::print、fmt::println 和 fmt::format,不依赖标准库的 <print> 或 <format>。
以下示例使用 {fmt} 11+、C++20。旧版若没有 fmt::println,使用 fmt::print("...\n", ...) 追加换行。
基本输出与运行时格式串
fmt::print 不追加换行,fmt::println 追加换行,二者默认写到 stdout;可以传入 FILE* 指定其他输出目标。它们同样不因换行就保证刷新缓冲区。
1 | |
fmt::runtime 显式选择运行时格式串检查,格式无效时抛出 fmt::format_error;它不是 C++23 的 std::print 接口。只输出外部文本时仍应写 fmt::println("{}", text),不要把文本当作格式串。
range 输出与自定义分隔符
range 和 tuple 支持需要包含 <fmt/ranges.h>。常见容器可以直接输出;需要修改分隔符时,使用 {fmt} 自己提供的 fmt::join:
1 | |
直接格式化 range 时,{::02x} 的第二个冒号引出元素格式;格式化 fmt::join 的结果时,{:02x} 就直接设置各元素的格式。fmt::join 本身不添加括号,也不是标准库 API;其返回值是借用原 range 的视图,宜像示例这样在源对象有效期间直接格式化。
头文件与标准接口的区别
| 功能 | {fmt} 头文件 / 接口 | 注意事项 |
|---|---|---|
| 输出、生成字符串 | <fmt/format.h>,fmt::print、fmt::println、fmt::format | 不需要 <print> 或 <format> |
| range / tuple / join | <fmt/ranges.h> | 支持范围及格式细节以所用 {fmt} 版本为准 |
| 时间格式化 | <fmt/chrono.h> | 使用 {fmt} 提供的 chrono 格式化器 |
输出到 std::ostream | <fmt/ostream.h>,fmt::print(stream, ...) | 不要假定所有标准输出重载都有完全相同的 {fmt} 重载 |
| 运行时格式串 | fmt::runtime(pattern) | 异常类型为 fmt::format_error,不是 std::format_error |
| 自定义类型 | fmt::formatter<T> | std::formatter<T> 不会自动被 {fmt} 复用,反向也一样 |
常用的 {}、索引、数值精度和对齐写法相近,但 {fmt} 不是标准库的命名空间别名;扩展功能、range 格式细节、版本支持和输出实现并非完全相同。不要把本文的 C++23 规则不加核对地当成任意 {fmt} 版本的完整契约,也不要通过向 std 命名空间添加别名来伪装标准接口。
实践建议
- 优先使用字面量格式串,让编译器尽早发现错误。
- 一条完整记录使用一次
std::println;不要为了换行而拼接"\n"。 - 数值格式要显式表达意图,例如
{:08x}、{:.2f},不要依赖默认格式作为协议的一部分。 - 对业务自定义类型提供小而明确的
std::formatter;不要修改或特化标准库类型。 - 标准库尚未提供
std::print时,可使用 {fmt};固定依赖版本,并核对格式说明和输出语义。
总结
std::print 将 std::format 的类型安全格式化能力带到了标准输出接口。新代码可优先使用 std::println 输出完整记录,用 std::print 处理需要自行控制换行的场景;遇到运行时格式串、locale 或并发日志时,再针对相应边界条件做显式处理。标准库支持不足时,{fmt} 提供了成熟的独立实现,但两套 API 及自定义格式化器不能直接混用。






