C++23 引入了 std::printstd::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 代码互操作,后者适合既有的流接口。两种形式都不改变格式串语法。

格式串规则

格式串由普通字符、替换字段和转义大括号组成:

1
2
3
std::println("{}", 42);                 // 42
std::println("{1} {0}", "first", "second"); // second first
std::println("{{{}}}", "value"); // {value}

{} 使用自动索引,{0}{1} 使用手动索引。同一个格式串不能混用两种索引方式;字面量大括号必须写成 {{` 或 `}}

替换字段的一般形式为 {索引:格式说明}。常用格式说明如下:

目的写法示例
默认格式{}std::println("{}", 42)42
左 / 右 / 居中{:<10} / {:>10} / {:^10}字符串默认左对齐,数值默认右对齐
自定义填充{:*^10}"hi"****hi****
显示正号{:+}42+42
零填充数值{:08}4200000042
动态宽度、精度{:>{}.{}f}由后续参数提供宽度和精度
转义字符串{:?}便于在日志中看清换行、引号等字符

宽度是最小显示宽度,不会截断较长内容。对齐默认值取决于类型:字符串、字符和 bool 默认左对齐;数值和指针默认右对齐。

1
2
3
4
5
6
std::println("|{:>10}|", "test");  // |      test|
std::println("|{:<10}|", "test"); // |test |
std::println("|{:^10}|", "test"); // | test |
std::println("|{:*^10}|", "test"); // |***test***|

std::println("{:>{}.{}f}", 3.14159, 8, 2); // 3.14

整数与进制

类型说明符前必须有冒号;{x} 表示索引名而不是十六进制格式。

1
2
3
4
5
6
7
8
9
10
11
12
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("{:#x}", value); // 0xff
std::println("{:#X}", value); // 0XFF
std::println("{:#o}", value); // 0377
std::println("{:#b}", value); // 0b11111111

# 选择替代形式,会为适用的进制加前缀。要让符号、前缀位于零填充之前,使用 0 选项而不是手写前置填充:

1
2
std::println("{:+08.2f}", 3.1); // +0003.10
std::println("{:#08x}", 42); // 0x00002a

浮点数与精度

类型含义示例
f / F定点表示;精度为小数点后位数{:.2f}3.14
e / E科学计数法;精度为小数点后位数{:.2e}3.14e+00
g / G通用表示;精度为有效数字位数{:.3g}3.14
a / A十六进制浮点表示{:a}42.01.5p+5

未指定类型时使用通用表示。未指定精度时,库会选择足以恢复原值的紧凑表示;因此不要把默认输出误认为固定为六位有效数字。

1
2
3
4
5
6
const double pi = 3.14159265359;

std::println("{}", pi); // 3.14159265359
std::println("{:.2}", pi); // 3.1,两个有效数字
std::println("{:.4}", pi); // 3.142,四个有效数字
std::println("{:.2f}", pi); // 3.14,两位小数

字符串精度

字符串的精度表示最长显示前缀,可用于截断;与宽度组合时,先截断再对齐。

1
2
3
4
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::print 的格式参数是 std::format_string。因此,字面量格式串会在编译期校验参数数量、索引和类型;运行时取得的 std::string_view 不能直接作为它的格式串。

1
2
3
4
5
6
7
8
9
10
11
12
#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 的范围、时间与本地化格式化

范围

C++23 为常见范围提供了格式化支持,通常不需要再为 std::vector 等标准容器编写格式化器:

1
2
3
4
5
#include <print>
#include <vector>

const std::vector values{1, 2, 3};
std::println("values = {}", values); // values = [1, 2, 3]

不要为标准库类型特化 std::formatter;这类特化不在用户可定制范围内。需要不同的容器展示风格时,单独编写格式化函数更清晰。

时间

<chrono> 类型可以直接使用时间格式说明:

1
2
3
4
5
6
#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 时间点本身。若需要按特定时区展示,应先构造对应时区的 zoned_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

自定义类型

为自定义类型特化 std::formatter 后,它就能被 std::printstd::format 等所有格式化接口复用。下面的格式化器只支持默认的 {} 形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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::printstd::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%pprintf 说明符与 {} 语法没有一一对应的文本替换;
  • std::print("%d", value) 会原样输出 %d,不会格式化 value
  • 依赖 printf 返回值的代码需要重新设计;
  • 为保持旧代码刻意指定的有符号/无符号输出语义,必要时显式 static_cast

实践建议

  1. 优先使用字面量格式串,让编译器尽早发现错误。
  2. 一条完整记录使用一次 std::println;不要为了换行而拼接 "\n"
  3. 数值格式要显式表达意图,例如 {:08x}{:.2f},不要依赖默认格式作为协议的一部分。
  4. 对业务自定义类型提供小而明确的 std::formatter;不要修改或特化标准库类型。
  5. 迁移旧代码时,确认返回值、缓冲、locale 和并发输出行为,而不只检查屏幕上的文本。

总结

std::printstd::format 的类型安全格式化能力带到了标准输出接口。新代码可优先使用 std::println 输出完整记录,用 std::print 处理需要自行控制换行的场景;遇到运行时格式串、locale、并发日志或旧 printf 迁移时,再针对相应边界条件做显式处理。

参考