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
2
3
4
5
#include <version>

#if !defined(__cpp_lib_print)
#error "This standard library does not support std::print"
#endif

基本用法

1
2
3
4
5
6
7
8
9
10
11
#include <print>
#include <string>

int main() {
std::print("Hello, World!\n");
std::println("Hello, World!");

const int age = 25;
const std::string name = "Alice";
std::println("Name: {}, Age: {}", name, age);
}

在大多数日志和报表场景中,std::println 更简洁;需要分段输出、进度条或自行控制换行时再使用 std::print。

输出到其他目标

除默认输出外,还可以传入 C 的 FILE* 或 C++ 的 std::ostream:

1
2
3
4
5
6
#include <cstdio>
#include <iostream>
#include <print>

std::println(stderr, "error: cannot open {}", "config.toml");
std::println(std::cerr, "error code: {}", 42);

前者适合与 printf/fprintf 代码互操作,后者适合既有的流接口。两种形式都不改变格式串语法。

格式串规则

以下以 C++23 为基线。std::print、std::println 与 std::format 使用相同的格式串规则,但不同参数类型拥有各自的格式说明语法,不能把数值格式说明直接套到 range 或时间类型上。

替换字段、索引与大括号

格式串由普通字符、替换字段和转义大括号组成。替换字段的形式为 {arg-id} 或 {arg-id:format-spec};索引和格式说明都可以省略,{} 与 {:} 都使用默认格式。

1
2
3
4
5
std::println("{}", 42); // 42
std::println("{1} {0}", "first", "second"); // second first
std::println("{0} / {0}", 42); // 42 / 42
std::println("{{{}}}", "value"); // {value}
std::println("100%"); // 100%
  • 索引从 0 开始,只能是十进制整数;标准接口不支持 {name} 这样的命名参数,也不支持 {0.member} 或 {0[1]} 这样的表达式。
  • {} 按出现顺序使用自动索引;{0}、{1} 使用手动索引,可以重排、重复引用或跳过参数。
  • 同一格式串不能混用自动索引与手动索引,动态宽度、精度中的索引也参与这项限制。
  • 字面量大括号必须写成 {{、}};反斜杠不能转义格式串的大括号。\n、\t 则是 C++ 字符串字面量的转义,先由语言处理。
  • 参数可以多于实际使用的数量,但引用不存在的参数无效。std::println("{}", 1, 2) 合法,只输出 1。

类型说明符前必须有冒号:{:x} 是十六进制格式,{x} 不是合法索引,也不是十六进制格式。

标准格式说明的完整顺序

基本算术类型、指针和字符串使用以下语法。方括号表示可选部分,不是要写进格式串的字符:

1
[[fill]align][sign][#][0][width][.precision][L][type]
部分可用写法含义与限制
fill单个填充字符,默认空格必须紧邻并位于 align 前;不能是 { 或 }
align<、>、^左对齐、右对齐、居中
sign+、-、空格正负都显示符号、只显示负号、非负值前放空格
##替代形式:整数进制前缀、浮点小数点及部分尾随零
00数值零填充,放在符号及进制前缀之后
width正十进制整数或 {}、{索引}最小显示宽度,不截断内容
.precision.非负整数、.{}、.{索引}仅浮点数、字符串支持;语义取决于类型
LL算术类型的 locale 格式化,详见本地化一节
type类型说明符必须与实参类型兼容,详见下表及各类型小节

顺序固定。例如 {:+#08x} 合法,{:8+x} 不合法。所有选项可省略,但并不是每种类型都支持所有选项。

实参类型默认形式C++23 支持的类型说明符
整数,不含 bool 和格式字符类型十进制b B c d o x X
char,用于窄字符格式串单个字符c ? b B d o x X
booltrue / falses b B d o x X
浮点数可恢复原值的最短表示a A e E f F g G
字符串、字符串视图、C 字符串原文s ?
void*、const void*、nullptr0x 前缀的地址p

这里的类型说明符不是转换操作。整数不能用 f,浮点数不能用 d;需要改变类型时应先显式转换。std::print 的格式串是窄字符形式,不能因此直接输出 std::wstring、char8_t 字符串等不同字符类型的文本。

对齐、填充与显示宽度

宽度是最小显示宽度,包含符号、进制前缀等最终输出内容。较长内容原样保留。字符串、字符及文本形式的 bool 默认左对齐;数值及指针默认右对齐。字符与 bool 使用整数说明符时也默认右对齐。

1
2
3
4
5
6
7
8
std::println("|{:>10}|", "test"); // |      test|
std::println("|{:<10}|", "test"); // |test |
std::println("|{:^10}|", "test"); // | test |
std::println("|{:*^10}|", "test"); // |***test***|
std::println("|{:*^6}|", 'x'); // |**x***|
std::println("|{:3}|", "abcdef"); // |abcdef|
std::println("|{:6}|", true); // |true |
std::println("|{:6d}|", true); // | 1|

居中时若填充数为奇数,右侧比左侧多一个填充字符。fill 在 Unicode 编码下可以是单个 Unicode 标量值,但库计算填充数时按每个填充字符宽度为 1 处理;用双列汉字或 emoji 填充可能造成视觉错位。

文本宽度不是字节数,也不简单等于 std::string::size()。Unicode 文本按显示宽度估计,标准建议以扩展字素簇及其首码点估算,部分宽字符计为两列。实际终端的 emoji、组合字符、ANSI 控制序列显示效果可能不同,表格排版仍应在目标终端验证。

符号、替代形式与零填充

sign 的默认值为 -。+ 为非负值加正号,空格为非负值留一列符号位置;负数始终保留负号,浮点负零也保留负号。这些规则同样适用于浮点无穷大和 NaN。

1
2
3
4
5
6
7
std::println("{:+} / {: } / {:-}", 42, 42, 42);
// +42 / 42 / 42
std::println("{:+08.2f}", 3.1); // +0003.10
std::println("{:#08x}", 42); // 0x00002a
std::println("{:08}", -42); // -0000042
std::println("{:0>8}", -42); // 00000-42
std::println("|{:<08}|", -42); // |-42 |

0 是数值选项,区别于 0> 中的普通填充字符:前者保留符号和前缀的位置,后者只在整段输出前填充。显式指定对齐后,0 选项不再产生零填充;无穷大与 NaN 也不做数值零填充。

sign、#、0 用于数值;字符和 bool 只有选择整数说明符后才能使用这些选项。字符串不支持它们。以字符形式输出的 c 也不能带数值符号、进制前缀或数值零填充。C++23 的指针不支持 0 选项和大写 P,这两项属于 C++26 的增强,不能因某个库提前支持就视为 C++23 的可移植写法。

整数与进制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const int value = 255;

std::println("{:d}", value); // 255
std::println("{:x}", value); // ff
std::println("{:X}", value); // FF
std::println("{:o}", value); // 377
std::println("{:b}", value); // 11111111
std::println("{:B}", value); // 11111111

std::println("{:#x}", value); // 0xff
std::println("{:#X}", value); // 0XFF
std::println("{:#o}", value); // 0377
std::println("{:#b}", value); // 0b11111111
std::println("{:#B}", value); // 0B11111111
std::println("{:#x}", -42); // -0x2a
std::println("{:#o}", 0); // 0

# 对十进制没有额外前缀;八进制只在非零值前补 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
2
3
4
5
6
7
8
9
10
11
12
const double pi = 3.14159265359;

std::println("{}", pi); // 3.14159265359
std::println("{:g}", pi); // 3.14159
std::println("{:.2}", pi); // 3.1
std::println("{:.4}", pi); // 3.142
std::println("{:.2f}", pi); // 3.14
std::println("{:.2e}", pi); // 3.14e+00
std::println("{:a}", 42.0); // 1.5p+5
std::println("{:.0f}", 3.0); // 3
std::println("{:#.0f}", 3.0); // 3.
std::println("{:#.4g}", 3.0); // 3.000

精度控制转换时的舍入,不是简单裁剪字符串。# 让有限浮点值即使没有小数位也保留小数点;对 g / G 还保留尾随零。小写说明符输出 inf、nan,大写说明符输出 INF、NAN。

动态宽度与动态精度

宽度、精度可以引用其他参数,但不能写表达式,也不能继续嵌套格式说明:

1
2
3
4
5
6
std::println("|{:>{}.{}f}|", 3.14159, 8, 2);
// | 3.14|
std::println("|{0:>{1}.{2}f}|", 3.14159, 8, 2);
// | 3.14|
std::println("|{1:>{0}}|", 6, "hi");
// | hi|

第一个例子依次使用参数 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
2
3
4
5
6
7
8
9
10
const std::string text = "This is a very long text";

std::println("|{:.10}|", text); // |This is a |
std::println("|{:>20.10}|", text); // | This is a |
std::println("{:s}", "hello"); // hello
std::println("{:?}", "a\n\"b\""); // "a\n\"b\""
std::println("{:?}", '\n'); // '\n'
std::println("{:c}", 65); // A,假设 ASCII 兼容编码
std::println("{:d}", 'A'); // 65,假设 ASCII 兼容编码
std::println("{} / {:d}", true, true); // true / 1

? 为字符串加双引号,为字符加单引号;对换行、制表、回车、引号、反斜杠等生成转义,对其他部分不可显示字符使用 \u{...},对无效编码单元使用 \x{...}。它不是 JSON 序列化器,不应拿调试输出作为交换协议。bool 不支持 ?,单个字符也不支持精度。

const char* 默认按以 '\0' 结尾的 C 字符串处理,不按地址输出;空指针不能作为有效 C 字符串。需要保留内嵌零字符时,传入带长度的 std::string 或 std::string_view。

指针与地址

标准地址格式化直接支持 void*、const void* 和 nullptr;其他对象指针需要显式转换。字符指针要看地址时也必须转换,不能仅写 {:p} 来改变其字符串语义。

1
2
3
4
5
6
int value = 42;
const char* text = "hello";

std::println("{:p}", static_cast<const void*>(&value));
std::println("{:p}", static_cast<const void*>(text));
std::println("{}", nullptr); // 0x0

地址值依赖运行环境;{: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
2
3
4
5
6
7
8
9
10
11
12
13
#include <format>
#include <print>
#include <string_view>

void print_runtime(std::string_view format,
std::string_view name, double score) {
// C++23:先格式化,再以固定格式串输出。
const auto message = std::vformat(
format, std::make_format_args(name, score));
std::println("{}", message);
}

print_runtime("name={}, score={:.1f}", "Alice", 95.5);

运行时格式串无效时,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <format>
#include <map>
#include <print>
#include <set>
#include <string>
#include <vector>

const std::vector values{1, 2, 3};
const std::set keys{3, 1, 2};
const std::map<std::string, int> counts{
{"b", 2}, {"a", 1}
};
const std::vector<std::string> words{"a", "b\nc"};

std::println("{}", values); // [1, 2, 3]
std::println("{}", keys); // {1, 2, 3}
std::println("{}", counts); // {"a": 1, "b": 2}
std::println("{}", words); // ["a", "b\nc"]
std::println("{}", std::vector<int>{}); // []

输出顺序就是迭代顺序:有序关联容器按比较器顺序,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
[[fill]align][width][n][range-type][:element-spec]
部分写法作用
外层填充、对齐、宽度*^20、>20、>{}对整段 range 输出生效,默认左对齐
nn去掉当前层的开闭括号,保留分隔符
range-typem、s、?smap 形式、字符串形式、转义字符串形式
element-spec再写一个 : 后接元素格式给每个元素使用同一份格式说明

外层没有 sign、#、数值零填充、精度或 L;这些选项若适用于元素,应写进 element-spec。range 的填充字符还不能是 :,以免与元素格式的起始冒号冲突。选项顺序固定,去括号的 map 形式写 nm,不是 mn。

第一个冒号引出 range 格式说明,第二个冒号才引出元素格式说明。{:>12} 对齐整个 range,{::>4} 对齐每个元素。外层宽度包含括号、分隔符及元素格式化后的内容,不是元素数量上限。

1
2
3
4
5
6
7
8
9
10
11
const std::vector values{1, 2, 3};

std::println("|{:>12}|", values); // | [1, 2, 3]|
std::println("|{:*>12n}|", values); // |*****1, 2, 3|
std::println("{::04d}", values); // [0001, 0002, 0003]
std::println("{:n:02x}", values); // 01, 02, 03
std::println("|{:>20:02}|", values);
// | [01, 02, 03]|
std::println("{::#x}", values); // [0x1, 0x2, 0x3]
std::println("{::.2f}", std::vector{1.25, 2.5});
// [1.25, 2.50]

{:n} 只去掉当前层括号,不是 join 操作,不会去掉逗号和空格。空 range 使用 {:n} 得到空文本;若同时有宽度,仍会产生填充。

字符与字符串元素的默认调试格式

未显式写 element-spec 时,range 会为支持 set_debug_format() 的元素格式化器启用调试格式;标准字符串和字符格式化器因此带引号及转义。显式写第二个冒号,即使后面为空,也会取消该层自动启用的调试格式:

1
2
3
4
5
6
7
8
9
10
11
const std::vector<std::string> words{"a", "b"};
const std::vector chars{'a', 'b'};

std::println("{}", words); // ["a", "b"]
std::println("{::}", words); // [a, b]
std::println("{::s}", words); // [a, b]
std::println("{::?}", words); // ["a", "b"]
std::println("{:n}", words); // "a", "b"
std::println("{:n:}", words); // a, b
std::println("{}", chars); // ['a', 'b']
std::println("{::}", chars); // [a, b]

指定字符串元素宽度或精度而仍想保留调试形式时,要显式加 ?,例如 {::>8?}。这条规则不意味着 tuple 的字段也能关闭调试格式:tuple 有自己的格式化规则,详见下文。

s 与 ?s:把字符 range 当作字符串

s 和 ?s 只适用于元素类型与格式串字符类型相同的 range;对于 std::print,就是 char range。vector<int>、vector<unsigned char>、字符串的 range 都不能这样使用。

1
2
3
4
5
6
const std::vector chars{'h', 'i', '\n'};

std::println("{:s}", chars); // hi,然后实际换行
std::println("{:?s}", chars); // "hi\n"
std::println("|{:>8s}|", std::vector{'h', 'i'});
// | hi|

s 按 range 的全部字符拼接,不以零字符提前结束;?s 再为整段字符生成带双引号的转义表示。{:s} 的内嵌换行与 std::println 追加的换行都有效,示例会产生两个换行。

两种形式都能指定外层填充、对齐、宽度,但不能与 n 或元素格式组合,也不支持外层精度。{:ns}、{:s:}、{:.3s} 都无效。要限制输出字符范围,应先生成有限视图或带长度的字符串视图。

m:以键值对形式输出

m 要求每个元素是 std::pair 或恰好含两个字段的 std::tuple。它把外层括号改为 {},把每个元素改为 key: value,条目之间仍为 ,。它只改变显示,不会排序、合并重复键或构造关联容器。

1
2
3
4
5
6
7
8
9
10
11
12
#include <utility>

const std::vector pairs{
std::pair{1, 2}, std::pair{3, 4}
};

std::println("{}", pairs); // [(1, 2), (3, 4)]
std::println("{:m}", pairs); // {1: 2, 3: 4}
std::println("{:nm}", pairs); // 1: 2, 3: 4
std::println("{:n}", pairs); // (1, 2), (3, 4)
std::println("{::m}", pairs); // [1: 2, 3: 4]
std::println("{::n}", pairs); // [1, 2, 3, 4]

注意 {: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
[[fill]align][width][tuple-type]

tuple-type 只能省略、写 n 或写 m,二者不能组合。默认以 (a, b, ...) 输出;n 去圆括号但保留 ,;m 仅适用于两个字段,去圆括号并把分隔符换成 :。外层宽度、对齐作用于整个 tuple,字段字符串、字符默认调试输出。

1
2
3
4
5
6
7
8
9
10
11
#include <tuple>
#include <utility>

const auto entry = std::pair{std::string{"a"}, 1};

std::println("{}", entry); // ("a", 1)
std::println("{:n}", entry); // "a", 1
std::println("{:m}", entry); // "a": 1
std::println("{}", std::tuple{1, true, 'x'});
// (1, true, 'x')
std::println("{}", std::tuple{}); // ()

tuple 格式不支持额外的元素格式冒号,不能给各字段设置独立精度或类型。需要控制字段时,应把字段拆成独立替换参数,或使用自己的包装类型及格式化器。

嵌套 range 与动态参数

每下降一层 range,就多经过一个元素格式冒号。嵌套括号不会被外层 n 自动去掉:

1
2
3
4
5
6
7
8
9
10
const std::vector<std::vector<int>> rows{
{1, 2}, {3, 4}
};

std::println("{}", rows); // [[1, 2], [3, 4]]
std::println("{:n}", rows); // [1, 2], [3, 4]
std::println("{::n}", rows); // [1, 2, 3, 4]
std::println("{:::02}", rows);
// [[01, 02], [03, 04]]
std::println("{:n:n:02}", rows); // 01, 02, 03, 04

这里 {:::02} 中第一个冒号引出外层格式,第二个冒号引出内层 range 格式,第三个冒号引出整数元素格式。去掉两层括号只是让显示变平,不改变数据结构或分隔符。

1
2
3
4
5
6
const std::vector values{1, 2};
const std::vector decimals{1.25, 2.5};

std::println("|{0:>{1}:{2}}|", values, 12, 3);
// | [ 1, 2]|
std::println("{::.{}f}", decimals, 1); // [1.2, 2.5]

动态宽度、精度仍遵循全局索引规则。元素格式只解析一次,因此同一个动态参数会应用于所有元素,并不是每个元素再消费一份参数。

视图、适配器与使用边界

1
2
3
4
5
6
7
8
9
#include <ranges>

const std::vector values{1, 2, 3, 4};
auto evens = values | std::views::filter([](int value) {
return value % 2 == 0;
});

std::println("{}", evens); // [2, 4]
std::println("{}", std::views::iota(1, 4)); // [1, 2, 3]
  • 视图不必先物化成容器;格式化时会实际遍历并计算元素。非 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
[[fill]align][width][.precision][L][chrono-specs]
转换符含义
%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、%VISO 周历年、两位 ISO 周历年、ISO 周数;周历年不一定等于日历年
%H、%I、%M、%S24 小时制小时、12 小时制小时、分、秒
%R、%T%H:%M、%H:%M:%S
%p、%rAM/PM、本地化的 12 小时制时间
%c、%x、%X本地化的日期时间、日期、时间
%Q、%qduration 的数值、单位后缀
%z、%Ez / %Oz、%ZUTC 偏移 +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
2
3
4
5
6
7
8
#include <chrono>
#include <print>

std::println("{}", std::chrono::milliseconds{42}); // 42ms
std::println("{:%Q %q}", std::chrono::milliseconds{42});
// 42 ms
std::println("{:%T}", std::chrono::seconds{3661});
// 01:01:01

不指定 chrono-specs 时使用类型默认的文本形式。<chrono> 类型也可以直接指定日期时间布局:

1
2
3
4
5
6
7
#include <chrono>
#include <print>

const auto now = std::chrono::floor<std::chrono::seconds>(
std::chrono::system_clock::now());
std::println("{:%F %T}", now);
// 例如:2025-11-04 02:57:46

这里格式化的是 system_clock 时间点本身,按 UTC 展示;若需要按特定时区展示,应先构造对应时区的 zoned_time。sys_time 可输出 UTC 的 %z 和 %Z,但一般的 local_time 没有时区信息,不能凭它生成偏移量或时区名称。

本地化

L 选项使用格式化上下文的 locale。std::print 本身没有 locale 参数,可先用 std::format 生成字符串:

1
2
3
4
5
6
#include <format>
#include <locale>
#include <print>

const std::locale locale{"en_US.UTF-8"};
std::println("{}", std::format(locale, "{:L}", 1'000'000));

分组符号、十进制符号和可用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <format>
#include <print>

struct Point {
int x;
int y;
};

template <>
struct std::formatter<Point> {
constexpr auto parse(std::format_parse_context& ctx) {
return ctx.begin(); // 仅接受空格式说明
}

auto format(const Point& point,
std::format_context& ctx) const {
return std::format_to(
ctx.out(), "({}, {})", point.x, point.y);
}
};

std::println("point = {}", Point{10, 20}); // point = (10, 20)

只能为程序定义的类型定制 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <fmt/format.h>

#include <cstdio>
#include <string>

int main() {
fmt::print("Hello, {}!\n", "World");
fmt::println("value={:#08x}, pi={:.2f}", 42, 3.14159);
// value=0x00002a, pi=3.14
fmt::println(stderr, "error code: {}", 42);

const std::string pattern = "name={}, score={:.1f}";
fmt::println(fmt::runtime(pattern), "Alice", 95.5);
// name=Alice, score=95.5
}

fmt::runtime 显式选择运行时格式串检查,格式无效时抛出 fmt::format_error;它不是 C++23 的 std::print 接口。只输出外部文本时仍应写 fmt::println("{}", text),不要把文本当作格式串。

range 输出与自定义分隔符

range 和 tuple 支持需要包含 <fmt/ranges.h>。常见容器可以直接输出;需要修改分隔符时,使用 {fmt} 自己提供的 fmt::join:

1
2
3
4
5
6
7
8
9
10
11
#include <fmt/format.h>
#include <fmt/ranges.h>

#include <vector>

const std::vector values{1, 2, 3};

fmt::println("{}", values); // [1, 2, 3]
fmt::println("{::02x}", values); // [01, 02, 03]
fmt::println("{}", fmt::join(values, "; ")); // 1; 2; 3
fmt::println("{:02x}", fmt::join(values, ":")); // 01:02:03

直接格式化 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 命名空间添加别名来伪装标准接口。

实践建议

  1. 优先使用字面量格式串,让编译器尽早发现错误。
  2. 一条完整记录使用一次 std::println;不要为了换行而拼接 "\n"。
  3. 数值格式要显式表达意图,例如 {:08x}、{:.2f},不要依赖默认格式作为协议的一部分。
  4. 对业务自定义类型提供小而明确的 std::formatter;不要修改或特化标准库类型。
  5. 标准库尚未提供 std::print 时,可使用 {fmt};固定依赖版本,并核对格式说明和输出语义。

总结

std::print 将 std::format 的类型安全格式化能力带到了标准输出接口。新代码可优先使用 std::println 输出完整记录,用 std::print 处理需要自行控制换行的场景;遇到运行时格式串、locale 或并发日志时,再针对相应边界条件做显式处理。标准库支持不足时,{fmt} 提供了成熟的独立实现,但两套 API 及自定义格式化器不能直接混用。

参考