C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++ 类型格式化format

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

作者:alwaysrun

C++20引入的std::format作为现代化字符串格式化工具,解决了传统printf类型不安全和stringstream语法冗长的问题,下面就来详细的介绍一下

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 零临时字符串)

关键差异:

核心 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("{} + {} = {}", 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"

最小宽度与动态宽度

int w = 8;
std::format("{:*>{}}", 123, w); // "*****123"

精度.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> 模板。

标准规格实现示例

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

窄整型符号扩展

当格式化 char 或 unsigned 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"

到此这篇关于C++之类型安全格式化format的文章就介绍到这了,更多相关C++ 类型格式化format内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文