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 代码互操作,后者适合既有的流接口。两种形式都不改变格式串语法。
格式串规则
格式串由普通字符、替换字段和转义大括号组成:
1 | |
{} 使用自动索引,{0}、{1} 使用手动索引。同一个格式串不能混用两种索引方式;字面量大括号必须写成 {{` 或 `}}。
替换字段的一般形式为 {索引:格式说明}。常用格式说明如下:
| 目的 | 写法 | 示例 |
|---|---|---|
| 默认格式 | {} | std::println("{}", 42) → 42 |
| 左 / 右 / 居中 | {:<10} / {:>10} / {:^10} | 字符串默认左对齐,数值默认右对齐 |
| 自定义填充 | {:*^10} | "hi" → ****hi**** |
| 显示正号 | {:+} | 42 → +42 |
| 零填充数值 | {:08} | 42 → 00000042 |
| 动态宽度、精度 | {:>{}.{}f} | 由后续参数提供宽度和精度 |
| 转义字符串 | {:?} | 便于在日志中看清换行、引号等字符 |
宽度是最小显示宽度,不会截断较长内容。对齐默认值取决于类型:字符串、字符和 bool 默认左对齐;数值和指针默认右对齐。
1 | |
整数与进制
类型说明符前必须有冒号;{x} 表示索引名而不是十六进制格式。
1 | |
# 选择替代形式,会为适用的进制加前缀。要让符号、前缀位于零填充之前,使用 0 选项而不是手写前置填充:
1 | |
浮点数与精度
| 类型 | 含义 | 示例 |
|---|---|---|
f / F | 定点表示;精度为小数点后位数 | {:.2f} → 3.14 |
e / E | 科学计数法;精度为小数点后位数 | {:.2e} → 3.14e+00 |
g / G | 通用表示;精度为有效数字位数 | {:.3g} → 3.14 |
a / A | 十六进制浮点表示 | {:a},42.0 → 1.5p+5 |
未指定类型时使用通用表示。未指定精度时,库会选择足以恢复原值的紧凑表示;因此不要把默认输出误认为固定为六位有效数字。
1 | |
字符串精度
字符串的精度表示最长显示前缀,可用于截断;与宽度组合时,先截断再对齐。
1 | |
对于包含宽字符或组合字符的文本,终端实际列宽可能与预期不同,表格排版时应在目标终端中验证。
运行时格式串
std::print 的格式参数是 std::format_string。因此,字面量格式串会在编译期校验参数数量、索引和类型;运行时取得的 std::string_view 不能直接作为它的格式串。
1 | |
运行时格式串无效时,std::vformat 会抛出 std::format_error。不要把不受信任的输入直接当作格式串;若只是输出用户提供的文本,应写成 std::println("{}", text)。
C++23 的范围、时间与本地化格式化
范围
C++23 为常见范围提供了格式化支持,通常不需要再为 std::vector 等标准容器编写格式化器:
1 | |
不要为标准库类型特化 std::formatter;这类特化不在用户可定制范围内。需要不同的容器展示风格时,单独编写格式化函数更清晰。
时间
<chrono> 类型可以直接使用时间格式说明:
1 | |
这里格式化的是 system_clock 时间点本身。若需要按特定时区展示,应先构造对应时区的 zoned_time,不要依赖运行环境的默认时区。
本地化
L 选项使用格式化上下文的 locale。std::print 本身没有 locale 参数,可先用 std::format 生成字符串:
1 | |
分组符号、十进制符号和可用 locale 名称均依赖平台;示例不应假设所有机器都输出 1,000,000。
自定义类型
为自定义类型特化 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。
从 printf 迁移
printf 风格 | std::print 风格 |
|---|---|
printf("value=%d\n", value); | std::println("value={}", value); |
fprintf(stderr, "open %s failed\n", path); | std::println(stderr, "open {} failed", path); |
printf("%#08x\n", value); | std::println("{:#08x}", value); |
printf("%.2f\n", pi); | std::println("{:.2f}", pi); |
迁移时应逐个检查格式语义,不能机械替换:
%d、%zu、%p等printf说明符与{}语法没有一一对应的文本替换;std::print("%d", value)会原样输出%d,不会格式化value;- 依赖
printf返回值的代码需要重新设计; - 为保持旧代码刻意指定的有符号/无符号输出语义,必要时显式
static_cast。
实践建议
- 优先使用字面量格式串,让编译器尽早发现错误。
- 一条完整记录使用一次
std::println;不要为了换行而拼接"\n"。 - 数值格式要显式表达意图,例如
{:08x}、{:.2f},不要依赖默认格式作为协议的一部分。 - 对业务自定义类型提供小而明确的
std::formatter;不要修改或特化标准库类型。 - 迁移旧代码时,确认返回值、缓冲、locale 和并发输出行为,而不只检查屏幕上的文本。
总结
std::print 将 std::format 的类型安全格式化能力带到了标准输出接口。新代码可优先使用 std::println 输出完整记录,用 std::print 处理需要自行控制换行的场景;遇到运行时格式串、locale、并发日志或旧 printf 迁移时,再针对相应边界条件做显式处理。









