C++20 引入的 std::format 标准格式化库,实质上是将广受欢迎的开源 fmt 库正式纳入 C++ 标准。这一全新接口一举解决了两个长期困扰开发者的痛点:传统 printf 函数存在的类型不安全问题,以及 stringstream 繁冗冗余的代码编写体验。从实际工程角度来看,std::format 如今已成为 C++ 格式化输出的首选方案,几乎没有替代者。
下面这张对比表清晰呈现了 printf、stringstream 与 std::format 之间的主要区别:
| 特性 | printf | stringstream | std::format |
|---|---|---|---|
| 类型安全 | 无编译期检查,类型不匹配会引发未定义行为 (UB) | 类型安全,借助 operator<< 重载机制 | 编译期验证格式字符串与参数类型,不匹配直接编译报错 |
| 自定义对象 | 原生不支持 | 需要重载 operator<< | 通过特化 std::formatter,逻辑更集中 |
| 格式语法 | %d/%f/%s 符号零散 | 操控符堆砌(如 setw/hex),代码冗长 | 统一 {} 占位符 + {:格式符} 语法 |
| 动态宽/精度 | 需 * 参数,易用性差 | 代码繁琐 | 原生支持 {:{width}} 和 {:.{prec}} 动态传参 |
| 性能 | 中等性能 | 性能较差(产生大量临时对象与堆分配) | 性能优秀(使用 format_to 可避免临时字符串生成) |
两处关键差异值得拎出来说说:
printf底层依赖于 C 风格的可变参数(varargs),编译器在编译期无法对参数类型进行有效检查。一旦类型匹配错误,运行时就会导致栈破坏或未定义行为,且这类 Bug 通常极难定位与调试。std::format则截然不同——它基于 C++ 可变参数模板(Variadic Templates)与编译期常量表达式(constexpr),在编译阶段直接解析格式字符串,对参数的数量和类型进行全面的静态强类型检查。只要不匹配,编译期就会直接报错,完全不会留到运行时。
核心 API 详解
std::format
提供最简洁的用法,直接返回格式化后的字符串。适用于大多数场景,特别是当最终结果仅需一个 std::string 时。
std::string s1 = std::format("name: {}, age: {}", "Jack", 20); // 输出: "name: Jack, age: 20"
std::format_to
高性能流式接口,直接将格式化结果写入指定的输出迭代器(如 std::vector、字符数组、std::ostream_iterator 等)。避免了中间临时 std::string 对象的分配开销,在性能敏感的代码中尤为实用。
// 写入字符容器(生产环境建议提前 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,主要用于封装自定义的可变参数函数——例如构建企业级日志系统或格式化包装器时,它扮演着底层基石的角色。
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"
}
占位符
格式字符串中的占位符遵循统一的结构:{[参数索引][:格式说明符]}。
无索引 {}
按参数传入的先后顺序,从左到右依次填充,最常用。
std::format("{} + {} = {}", 1, 2, 3); // "1 + 2 = 3"
数字索引 {N}
从 0 开始指定参数索引,可以调换输出顺序,甚至同一个参数复用多次。
std::format("{1}-{0}={1}", 5, 9); // "9-5=9"
有一点要注意:在同一个格式化字符串中,显式数字索引 {N} 和自动无索引 {} 不能混用,否则编译期会直接报语法错误。另外,C++ 标准库目前还不支持 {name} 这种具名关键参数——这个语法只在开源 fmt 库里可用。
格式说明符
格式说明符紧跟在冒号后面,各项的顺序是固定的:[填充字符][对齐][符号][#][0][宽度][.精度][类型码]
对齐 + 填充
<:左对齐>:右对齐(非数值类型默认)^:居中对齐- 填充字符:紧挨着对齐符号左侧的单个字符,默认是空格
int n = 123;
std::format("{:*>6}", n); // "***123"
std::format("{:*<6}", n); // "123***"
std::format("{:*^6}", n); // "*123**"
符号规则(数值类型)
+:正数输出+,负数输出--:仅负数输出-(标准默认行为)- (空格):正数前置补空格,负数输出
-
std::format("{:+d}", 20); // "+20"
std::format("{: d}", 20); // " 20"
std::format("{: d}", -20); // "-20"
# 进制前缀开关
自动为整型数据加上进制前缀标识:十六进制加 0x/0X,二进制加 0b,八进制加 0。
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 填充。
std::format("{:06d}", 123); // "000123"
最小宽度与动态宽度
- 固定宽度:
{:6d}表示输出至少占 6 个字符位。 - 动态宽度:
{:{w}}可以在运行时通过额外参数动态指定宽度。
int w = 8;
std::format("{:*>{}}", 123, w); // "*****123"
精度 .prec
- 浮点数:指定小数点后的保留位数。
- 字符串:指定最大截取字符数。
- 动态精度:使用
{:.{prec}}由后续参数动态控制。
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 字符输出 |
bool b = true;
std::format("{}", b); // "true"
std::format("{:d}", b); // "1"
自定义类型格式化
想让自定义类型也能使用 std::format 进行格式化?关键在于显式特化 std::formatter 模板。

下面是一个标准的实现示例:
#include
#include
#include // --- 1. 自定义数据类型 ---
struct User {
std::string name;
int age;
};// --- 2. 在 std 命名空间内为 User 类型特化 formatter ---
namespace std {
// 特化版本 1: 处理 char 类型的格式字符串 (e.g., std::format)
template <>
struct formatterchar > { // 显式指定第二个模板参数为 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 formatterwchar_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 将 时间库与 std::format 深度融合。现在可以直接对系统时间、持续时间进行高级格式化,非常顺手:
#include
#include
#include 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 异常。举个例子:
#include
#include
#include 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 特化必须显式放在 namespace std 里,否则格式化引擎进行 ADL 查找时找不到特化,直接编译错误。
窄整型符号扩展
格式化 char 或 unsigned char 变参并指定 {:d} 打印数值时,容易因为隐式符号扩展导致输出不符合预期的负数。在严谨的高性能场景下,建议用 static_cast 显式强转。
std::format("{:d}", static_cast<int>(ch));
高频使用示例
// 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"
