C++类型安全格式化format

作者:袖梨 2026-06-09

C++20 引入的 std::format 是基于开源 fmt 库标准化的新式格式化接口,解决了传统 printf 类型不安全、stringstream 代码冗余等问题,成为当前 C++ 首选的格式化方案。

与 printf 和 stringstream 对比:

特性printfstringstreamstd::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 的通用场景。

std::string s1 = std::format("name: {}, age: {}""Jack"20); // 返回 "name: Jack, age: 20"

std::format_to

高性能流式接口,直接将数据写入指定的输出迭代器(如 std::vector<char>、字符数组、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。该 API 主要用于封装自定义的可变参数函数,是构建企业级通用日志系统或格式化包装器的核心基石。

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("{} + {} = {}"123); // "1 + 2 = 3"

数字索引 {N}

从 0 开始指定参数索引,支持显式调换输出顺序以及同一个参数的多次复用。

std::format("{1}-{0}={1}"59); // "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<T> 模板。

img_6a275bb1e4fb030.webp

标准规格实现示例

#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 进行了深度融合。可直接对系统时间、持续时间进行高级格式化:

#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 异常。

#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 关联模板特化查找时会直接宣告失败并引发编译错误。

窄整型符号扩展

当格式化 charunsigned char 变参并指定 {:d} 打印数值时,容易因为隐式符号扩展导致输出不符合预期的负数数值。在严谨的高性能场景下,显式通过 static_cast<int> 进行强转切分。

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"

本文使用 markdown.com.cn 排版

相关文章

精彩推荐