更多 C++ 文章见《修远之路(C++集萃)》专栏
C++20 引入的 std::format 是基于开源 fmt 库标准化的新式格式化接口,解决了传统 printf 类型不安全、stringstream 代码冗余等问题,成为当前 C++ 首选的格式化方案。
与 printf 和 stringstream 对比:
| 特性 | printf | stringstream | std::format |
|---|---|---|---|
| 类型安全 | 无编译检查,类型错配导致未定义行为 (UB) | 类型安全,通过 operator<< 重载 |
编译期检查格式串与参数类型,错配即编译报错 |
| 自定义对象 | 不支持 | 需重载 operator<< |
特化 std::formatter<T>,逻辑集中 |
| 格式语法 | %d/%f/%s 符号零散 |
操控符堆砌(如 setw/hex),代码冗长 |
统一 {} 占位符 + {:格式符} 语法 |
| 动态宽/精度 | 需 * 参数,易用性差 |
代码繁琐 | 原生支持 {:{width}} 和 {:.{prec}} 动态传参 |
| 性能 | 中等 | 差(大量临时对象、堆分配) | 优秀(format_to 零临时字符串) |
关键差异:
printf使用裸可变参数(C-style varargs),编译器在编译阶段无法有效对参数类型进行强校验,极易在运行时因类型不匹配导致栈破坏或未定义行为。std::format则是基于 C++ 模板变参(Variadic Templates)与编译期常量表达式(constexpr)技术,在编译期直接解析字符串语法,对参数数量、参数类型进行全方位的静态强类型检查。
核心 API
std::format
生成格式化后的字符串并返回。适用于大部分需要便捷获取 std::string 的通用场景。
c
std::string s1 = std::format("name: {}, age: {}", "Jack", 20); // 返回 "name: Jack, age: 20"
std::format_to
高性能流式接口,直接将数据写入指定的输出迭代器(如 std::vector<char>、字符数组、std::ostream_iterator 等),完美避免了生成中间临时 std::string 对象的内存分配开销。
c
// 写入字符容器(生产环境建议提前 reserve 以保证性能)
std::vector<char> buf;
buf.reserve(32);
std::format_to(std::back_inserter(buf), "num = {}", 314);
// 直接零拷贝输出到控制台
std::format_to(std::ostream_iterator<char>(std::cout), "Hello {}\n", 666);
std::vformat
接收打包后的动态参数包 std::format_args。该 API 主要用于封装自定义的可变参数函数,是构建企业级通用日志系统或格式化包装器的核心基石。
c
void log_message(std::string_view fmt, auto... args) {
// 运行时或编译期将变参打包
std::format_args pack = std::make_format_args(args...);
// 传递给 vformat 执行实际的格式化转换
std::cout << std::vformat(fmt, pack) << '\n';
}
int main() {
log_message("val:{}", 123); // 输出 "val:123"
}
占位符
通用格式:{[参数索引][:格式说明符]}
无索引 {}
按照参数传入的先后顺序,从左到右依次填充。
c
std::format("{} + {} = {}", 1, 2, 3); // "1 + 2 = 3"
数字索引 {N}
从 0 开始指定参数索引,支持显式调换输出顺序以及同一个参数的多次复用。
c
std::format("{1}-{0}={1}", 5, 9); // "9-5=9"
注意: 在同一个格式化字符串中,显式数字索引 {N} 与自动无索引 {} 不能混用,否则会导致编译期语法错误。另外,C++ 标准库并未原生支持如 {name} 形式的具名关键参数(该语法目前仅在开源 fmt 库中支持)。
格式说明符
格式说明符紧跟在冒号后面,各控制分段的相对顺序严格固定:
[填充字符][对齐][符号][#][0][宽度][.精度][类型码]
对齐 + 填充
<:左对齐>:右对齐(非数值默认)^:居中对齐- 填充字符:紧邻对齐符号左侧的任意单个字符(默认是空格)
c
int n = 123;
std::format("{:*>6}", n); // "***123"
std::format("{:*<6}", n); // "123***"
std::format("{:*^6}", n); // "*123**"
符号规则(数值类型)
+:正数输出+,负数输出--:仅负数输出-(标准默认行为)- (空格):正数前置补空格,负数输出
-
c
std::format("{:+d}", 20); // "+20"
std::format("{: d}", 20); // " 20"
std::format("{: d}", -20); // "-20"
# 进制前缀开关
自动为整型数据启用前缀标识符:十六进制引入 0x/0X,二进制引入 0b,八进制引入 0。
c
int val = 15;
std::format("{:#x}", val); // "0xf"
std::format("{:#X}", val); // "0XF"
std::format("{:#b}", val); // "0b1111"
std::format("{:#o}", val); // "017"
0 前导补零
在宽度控制前加 0,空余位用字符 0 填充(本质上等价于右对齐且用 0 填充)。
c
std::format("{:06d}", 123); // "000123"
最小宽度与动态宽度
- 固定宽度:
{:6d}表示目标输出至少占 6 个字符位。 - 动态宽度:
{:{w}}运行时通过额外的参数动态指定宽度。
c
int w = 8;
std::format("{:*>{}}", 123, w); // "*****123"
精度 .prec
- 浮点数:指定小数点后的保留位数。
- 字符串:指定最大截取字符数。
- 动态精度:使用
{:.{prec}}由后续参数动态控制。
c
double pi = 3.1415926;
std::format("{:.3f}", pi); // "3.142"
std::string s = "abcdef";
std::format("{:.3}", s); // "abc"
int prec = 2;
std::format("{:.{}}", pi, prec); // "3.14"
类型码
不指定时将自动根据泛型推导
| 分类 | 标识 | 说明 |
|---|---|---|
| 整数 | d/o/x/X/b |
十进制 / 八进制 / 小写十六进制 / 大写十六进制 / 二进制 |
| 浮点 | f/e/g/a |
定点小数 / 科学计数法 / 自动精简 / 十六进制浮点 |
| 布尔 | s/d |
输出 true/false 或文本化的 1/0 |
| 指针 | p |
格式化输出内存物理地址 |
| 字符 | c |
将整型数值转换为对应的 ASCII 字符输出 |
c
bool b = true;
std::format("{}", b); // "true"
std::format("{:d}", b); // "1"
自定义类型格式化
让自定义类型支持 std::format 的核心在于显式特化 std::formatter<T> 模板。

标准规格实现示例
arduino
#include <format>
#include <string>
#include <iostream>
// --- 1. 自定义数据类型 ---
struct User {
std::string name;
int age;
};
// --- 2. 在 std 命名空间内为 User 类型特化 formatter ---
namespace std {
// 特化版本 1: 处理 char 类型的格式字符串 (e.g., std::format)
template <>
struct formatter<User, char> { // 显式指定第二个模板参数为 char
// 必须定义 char_type,告诉格式化库我们处理的是哪种字符
using char_type = char;
// 解析格式说明符的函数
constexpr auto parse(basic_format_parse_context<char>& ctx) const {
auto it = ctx.begin();
auto end = ctx.end();
// 检查格式说明符是否为空 (即 {})
if (it == end) {
// 空格式说明符,解析成功,直接返回
return it;
}
// 如果不为空,我们目前不支持任何格式化选项,
// 所以期望下一个字符必须是结束符 '}'
if (*it != '}') {
throw format_error("Invalid format specifier for User (char).");
}
// 解析成功,返回指向 '}' 的迭代器
return it;
}
// 执行实际格式化的函数
auto format(const User& u, format_context& ctx) const {
// 使用 format_to 将数据写入输出迭代器
return format_to(ctx.out(), "User[name={}, age={}]", u.name, u.age);
}
};
// 特化版本 2: 处理 wchar_t 类型的格式字符串 (e.g., std::wformat)
template <>
struct formatter<User, wchar_t> { // 显式指定第二个模板参数为 wchar_t
using char_type = wchar_t;
constexpr auto parse(basic_format_parse_context<wchar_t>& ctx) const {
auto it = ctx.begin();
auto end = ctx.end();
if (it == end) {
return it;
}
if (*it != L'}') { // 注意宽字符的 '}'
throw format_error(L"Invalid format specifier for User (wchar_t).");
}
return it;
}
auto format(const User& u, wformat_context& ctx) const {
// 注意使用 L"..." 宽字符串字面量
return format_to(ctx.out(), L"User[name={}, age={}]", u.name, u.age);
}
};
}
// --- 3. 主函数,测试我们的自定义格式化 ---
int main() {
User u{"Tom", 25};
// 测试 1: 使用 char 版本的 std::format
std::string res = std::format("{}", u);
std::cout << "std::format result: " << res << '\n';
// 测试 2: 使用 wchar_t 版本的 std::wformat
std::wstring wres = std::wformat(L"{}", u);
std::wcout << L"std::wformat result: " << wres << L'\n';
return 0;
}
时间格式化
C++20 将 <chrono> 时间库与 std::format 进行了深度融合。可直接对系统时间、持续时间进行高级格式化:
c
#include <chrono>
#include <format>
#include <iostream>
int main() {
auto now = std::chrono::system_clock::now();
// 原生支持时间轴格式化输出
std::cout << std::format("{:%Y-%m-%d %H:%M:%S}", now);
return 0;
}
// 示例输出:2026-06-03 23:37:18
时间格式控制符规则与传统标准 C 函数 strftime 完全一致(如 %Y 代表四位数年份,%m 代表月份,%d 代表日期),详情可参考《C++之时间日期库chrono》。
异常与校验
编译期严格校验
在代码中传入字面量格式串(Literal string)时,现代编译器会在编译阶段通过 constexpr 机制提前运行格式串解析器。一旦发现占位符数量与参数列表不匹配,或者类型对应的格式符错误(例如对 std::string 使用了 {:d}),将在编译期直接拦截并抛出编译错误。
运行时异常拦截
如果是动态组装、或运行时从配置文件读入的非常量格式化字符串,编译器无法做静态前置检查,错误将被推迟到运行期。此时格式化引擎会抛出 std::format_error 异常。
c
#include <iostream>
#include <format>
#include <string>
int main() {
try {
std::string dynamic_fmt = "{:z}"; // 'z' 是针对整型完全非法的未知格式符
std::format(dynamic_fmt, 1);
} catch (const std::format_error& e) {
// 优雅捕获运行期格式化异常,防止服务崩溃
std::cerr << "Format error caught: " << e.what() << std::endl;
}
}
总结
命名空间限制
自定义类型的 formatter<T> 特化必须显式置于 namespace std 空间内,否则格式化引擎在进行 ADL 关联模板特化查找时会直接宣告失败并引发编译错误。
窄整型符号扩展
当格式化 char 或 unsigned char 变参并指定 {:d} 打印数值时,容易因为隐式符号扩展导致输出不符合预期的负数数值。在严谨的高性能场景下,显式通过 static_cast<int> 进行强转切分。
c
std::format("{:d}", static_cast<int>(ch));
高频使用示例
c
// 1. 固定高位补零的十六进制大写输出
std::string hex_str = std::format("0x{:04X}", 255); // "0x00FF"
// 2. 浮点数四舍五入保留两位小数
std::string fp_str = std::format("{:.2f}", 2.71828); // "2.72"
// 3. 基于动态宽度的靠右填充边界对齐
int padding_width = 10;
std::string align_str = std::format("{:*>{}}", 99, padding_width); // "*******99"
本文使用 markdown.com.cn 排版