C++之类型安全格式化format

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

窄整型符号扩展

当格式化 charunsigned 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 排版

相关推荐
DogDaoDao1 小时前
【GitHub】 Open Design 深度技术解析:把 Claude Design 搬回本地的 Agent 设计工作台
深度学习·程序员·github·ai编程·claude·ai agent·open design
邪修king1 小时前
C++ 哈希表超全详解:从底层实现到封装 myunordered_map/myunordered_set
c++·哈希算法·散列表
secret_to_me1 小时前
buildRoot编译rootfs实战
linux·c语言·c++·ubuntu·电脑·buildroot
凡人叶枫1 小时前
Effective C++ 条款01:视 C++ 为一个语言联邦
linux·开发语言·c++·effective c++·编程范式·语言联邦
QiLinkOS1 小时前
合肥气链科技有限公司本质总结
c++·科技·算法·gitee·开源
Yuk丶1 小时前
厌倦了假AI对话?本地 LLM 语音对话 + 口型同步系统 2.0(已开源!)
c++·人工智能·语言模型·开源·ue4·语音识别·游戏开发
kyle~1 小时前
ROS2---零拷贝
linux·c++·机器人·ros2
Ricky_Theseus1 小时前
栈 & 队列 应用场景
数据结构·c++
薇茗1 小时前
【C++】类与对象 核心篇
开发语言·c++