C++ `std::format`:C++20 安全高效的格式化字符串

好的,各位观众,欢迎来到今天的C++奇妙夜(其实是技术讲座啦)。今天我们要聊的是C++20引入的std::format,一个能让你告别printfiostream,安全高效地格式化字符串的利器。

开场白:为什么我们需要std::format

话说江湖上一直流传着这么一句话:“C++的字符串格式化,那是一部血泪史。” 这话真不假。传统的printf,类型不安全,容易崩溃,调试起来让人头大。iostream呢,虽然类型安全了,但是语法又过于冗长,代码可读性直线下降。

举个例子,假设我们要格式化一个整数和一个浮点数:

  • printf:

    int age = 30;
    double salary = 100000.50;
    printf("Age: %d, Salary: %fn", age, salary); // 没毛病,编译通过
    printf("Age: %f, Salary: %dn", age, salary); // 编译也通过,运行时就等着看崩溃吧!

    printf的问题在于,它的格式化字符串和后面的参数类型匹配完全依赖程序员自己保证。一旦写错,编译器不会报错,运行时就给你一个惊喜(通常是不太愉快的惊喜)。

  • iostream:

    int age = 30;
    double salary = 100000.50;
    std::cout << "Age: " << age << ", Salary: " << salary << std::endl; // 还能更长点吗?

    iostream虽然类型安全,但是代码过于冗长,可读性差。想象一下,如果需要格式化很多变量,这代码得写多长?而且,对于一些复杂的格式化需求,iostream实现起来也很麻烦。

所以,我们需要一个既类型安全,又简洁易用的字符串格式化工具。这就是std::format登场的理由!

std::format:救星来了!

std::format解决了printf的类型安全问题,又避免了iostream的冗长,简直是C++程序员的福音。它的核心思想是:

  • 编译时类型检查: 格式化字符串中的占位符和后面的参数类型必须匹配,否则编译报错。
  • 简洁的语法: 使用花括号 {} 作为占位符,语法简洁明了。
  • 强大的格式化选项: 支持各种各样的格式化选项,可以满足各种需求。

基本用法:Hello, World! 的进阶版

先来看一个简单的例子:

#include <format>
#include <iostream>

int main() {
    int age = 30;
    double salary = 100000.50;
    std::string formatted_string = std::format("Age: {}, Salary: {:.2f}", age, salary);
    std::cout << formatted_string << std::endl; // 输出:Age: 30, Salary: 100000.50
    return 0;
}

这个例子中,{} 是占位符,分别对应后面的 agesalary{:.2f} 表示将 salary 格式化为保留两位小数的浮点数。

看到了吗?std::format 既简洁又类型安全。如果我们将 agesalary 的顺序写反,或者将 {: .2f} 写成 %d,编译器会直接报错,避免了运行时崩溃的风险。

格式化选项:玩转字符串

std::format 提供了丰富的格式化选项,可以控制数字、字符串、日期等等的显示方式。下面是一些常用的格式化选项:

选项 含义 示例
{} 默认格式化 std::format("{}", 123) // 输出 "123"
{:d} 十进制整数 std::format("{:d}", 123) // 输出 "123"
{:x} 十六进制整数(小写) std::format("{:x}", 255) // 输出 "ff"
{:X} 十六进制整数(大写) std::format("{:X}", 255) // 输出 "FF"
{:o} 八进制整数 std::format("{:o}", 8) // 输出 "10"
{:b} 二进制整数 (C++23) std::format("{:b}", 5) // 输出 "101"
{:f} 定点表示的浮点数 std::format("{:f}", 3.14159) // 输出 "3.141590"
{:e} 指数表示的浮点数(小写) std::format("{:e}", 1234.567) // 输出 "1.234567e+03"
{:E} 指数表示的浮点数(大写) std::format("{:E}", 1234.567) // 输出 "1.234567E+03"
{:g} 通用格式的浮点数(自动选择定点或指数表示) std::format("{:g}", 1234.567) // 输出 "1234.57"
{:G} 通用格式的浮点数(自动选择定点或指数表示,指数部分大写) std::format("{:G}", 1234.567) // 输出 "1234.57"
{:s} 字符串 std::format("{:s}", "hello") // 输出 "hello"
{:c} 字符 std::format("{:c}", 'A') // 输出 "A"
{:p} 指针 int x = 10; std::format("{:p}", &x) // 输出 "0x7ffc…" (地址)
{:n} 本地化数字分隔符(例如,千位分隔符) std::format("{:n}", 1234567) // 输出 "1,234,567" (取决于本地设置)
{:<width} 左对齐,指定宽度 std::format("{:<10}", "hello") // 输出 "hello "
{:>width} 右对齐,指定宽度 std::format("{:>10}", "hello") // 输出 " hello"
{:^width} 居中对齐,指定宽度 std::format("{:^10}", "hello") // 输出 " hello "
{:填充字符<width} 左对齐,指定宽度和填充字符 std::format("{:*<10}", "hello") // 输出 "hello*****"
{:填充字符>width} 右对齐,指定宽度和填充字符 std::format("{:*>10}", "hello") // 输出 "*****hello"
{:填充字符^width} 居中对齐,指定宽度和填充字符 std::format("{:*^10}", "hello") // 输出 "**hello***"
:.precision 指定精度(用于浮点数,表示小数点后的位数;用于字符串,表示最大长度) std::format("{:.2f}", 3.14159) // 输出 "3.14"; std::format("{:.5s}", "hello world") // 输出 "hello"
:+ 对于正数显示加号 std::format("{:+d}", 10) // 输出 "+10"
:- 对于负数显示减号(默认行为) std::format("{:-d}", -10) // 输出 "-10"
:{ } 在正数和负数之前都插入一个空格 std::format("{: d}", 10) // 输出 " 10"; std::format("{: d}", -10) // 输出 "-10"
:= 符号感知零填充 (将符号放在填充字符之前)。 与数字类型 (int, float, complex 等) 一起使用。 std::format("{:=+05d}", 10) // 输出 "+0010" ; std::format("{:=05d}", -10) // 输出 "-0010"
{#} 使用 # 选项通常会启用 "alternate form" 的格式化。 对于不同的类型,它的作用不同: – 对于整数类型 (例如 x, X, o, b),它会在输出中添加前缀来指示进制 (例如 0x, 0X, 0o, 0b)。 – 对于浮点数类型 (例如 f, e, E, g, G),它会确保输出始终包含小数点,即使小数点后没有数字。 std::format("{:#x}", 255) // 输出 "0xff" ; std::format("{:#.0f}", 10.0) // 输出 "10."
{:{}} 动态指定宽度和精度。 第一个 {} 指定要格式化的值,第二个 {} 指定宽度或精度。 这些值可以是参数列表中的其他位置的值,也可以是变量。 int width = 10; std::string name = "hello"; std::format("{:{}}", name, width); // 输出 "hello "; double value = 3.14159; int precision = 2; std::format("{:.{}}", value, precision); // 输出 "3.14"

示例代码:格式化各种数据类型

#include <format>
#include <iostream>
#include <string>
#include <iomanip> // 需要包含这个头文件才能使用 std::put_time

int main() {
    int age = 30;
    double salary = 100000.50;
    std::string name = "Alice";
    char grade = 'A';

    // 格式化整数
    std::cout << std::format("Age: {:d}, Age in hex: {:x}, Age in octal: {:o}", age, age, age) << std::endl;
    // 输出:Age: 30, Age in hex: 1e, Age in octal: 36

    // 格式化浮点数
    std::cout << std::format("Salary: {:.2f}, Salary in scientific notation: {:.2e}", salary, salary) << std::endl;
    // 输出:Salary: 100000.50, Salary in scientific notation: 1.00e+05

    // 格式化字符串
    std::cout << std::format("Name: {:s}, Name with width: {:>10}", name, name) << std::endl;
    // 输出:Name: Alice, Name with width:      Alice

    // 格式化字符
    std::cout << std::format("Grade: {:c}", grade) << std::endl;
    // 输出:Grade: A

    // 对齐和填充
    std::cout << std::format("Left align: {:<10}, Right align: {:>10}, Center align: {:^10}", "hello", "hello", "hello") << std::endl;
    // 输出:Left align: hello     , Right align:      hello, Center align:   hello

    std::cout << std::format("Left align with fill: {:*<10}, Right align with fill: {:*>10}, Center align with fill: {:*^10}", "hello", "hello", "hello") << std::endl;
    // 输出:Left align with fill: hello*****, Right align with fill: *****hello, Center align with fill: **hello***

    // 使用本地化设置
    std::cout << std::format("Number with thousands separator: {:n}", 1234567) << std::endl;
    // 输出:Number with thousands separator: 1,234,567 (取决于本地设置)

     //格式化布尔值
    bool is_valid = true;
    std::cout << std::format("Is valid: {}", is_valid) << std::endl; // 输出: Is valid: true

    // 格式化指针
    int number = 42;
    int* ptr = &number;
    std::cout << std::format("Address of number: {:p}", (void*)ptr) << std::endl; //  输出: Address of number: 0x...

    // 动态指定宽度和精度
    int width = 15;
    int precision = 3;
    double pi = 3.14159265359;
    std::cout << std::format("Pi with dynamic precision (width = {}, precision = {}): {:{}.{}}", width, precision, pi, width, precision) << std::endl;
    // 输出: Pi with dynamic precision (width = 15, precision = 3):          3.142

    return 0;
}

自定义格式化:打造专属风格

std::format 不仅提供了丰富的内置格式化选项,还允许我们自定义格式化方式。这可以通过自定义格式化器来实现。

(这个部分涉及较为高级的C++知识,为了保证文章的流畅性,我们这里只简单介绍一下概念,不深入展开。)

简单来说,我们需要定义一个类,重载 format 函数,然后在 std::format 中使用这个类。

性能:速度与激情

std::format 在设计时就考虑了性能。它通常比 iostream 更快,而且由于编译时类型检查,避免了运行时错误,也间接提高了性能。

printf 的对比:一场新老技术的对话

特性 printf std::format
类型安全 不安全,依赖程序员保证类型匹配 安全,编译时类型检查
语法 复杂,使用 %d, %f 等格式化字符串 简洁,使用花括号 {} 作为占位符
可读性 较差 良好
性能 通常较快 性能良好,有时更快,有时略慢,取决于具体情况
扩展性 差,难以自定义格式化方式 好,可以通过自定义格式化器扩展
易用性 对于简单格式化比较方便,复杂格式化比较麻烦 更加一致和易于使用

总的来说,std::format 在类型安全、可读性、扩展性等方面都优于 printf,是更现代、更推荐的选择。当然,在一些对性能要求极高的场景下,printf 可能仍然有其用武之地。

总结:拥抱 std::format,告别字符串格式化的烦恼

std::format 是 C++20 引入的一个强大的字符串格式化工具,它解决了传统 printfiostream 的痛点,提供了类型安全、简洁易用、功能强大的字符串格式化方案。

  • 类型安全: 避免运行时崩溃,提高代码质量。
  • 简洁易用: 语法简洁明了,提高开发效率。
  • 功能强大: 丰富的格式化选项,满足各种需求。
  • 性能良好: 速度快,效率高。

所以,还在等什么?赶快拥抱 std::format,让你的 C++ 代码更加优雅、安全、高效吧!

最后的彩蛋:一些使用技巧

  • 避免重复书写参数: 可以使用索引来指定参数的位置。例如:std::format("{1}, {0}", "world", "hello") 输出 "hello, world"。
  • 使用命名参数: 可以使用命名参数来提高代码可读性。例如:std::format("{name}, {age}", fmt::arg("name") = "Alice", fmt::arg("age") = 30) (需要引入 fmt 库,因为 std::format 本身不支持命名参数,但 fmt 库是 std::format 的灵感来源)。
  • 结合 ranges 使用: 可以使用 std::format 格式化 ranges 中的元素。

今天的讲座就到这里,希望大家有所收获! 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注