【C++ 格式化输出 】C++20 现代C++格式化

发布时间 2023-10-11 16:59:17作者: 冰山奇迹

现代C++格式化:拥抱std::format简化你的代码
1. 引言
传统C++格式化的问题与挑战
C++20引入std::format的背景
2. std::format简介
std::format的基本概念
std::format与printf、iostreams的对比
高效使用std::format的理由
3. 基本用法
格式字符串与占位符
类型规格与格式选项
4. 格式化数字
控制数字的宽度、精度与填充
显示或隐藏正负号
进制转换(十进制、十六进制、八进制等)
浮点数格式化选项
5. 格式化文本
控制字符串的宽度与填充
处理特殊字符与转义
使用std::format处理多语言与Unicode
6. 格式化日期与时间
使用chrono库处理时间点与持续时间
时间格式化选项
本地化日期与时间的显示
7. 自定义类型的格式化
实现自定义类型的格式化支持
使用fmt::formatter特化
示例:为自定义类型实现格式化输出
8. std::format的高级技巧与应用
格式字符串的动态生成
使用std::format与其他标准库组件(如容器、文件操作等)
提高格式化性能的建议
9. 结论与展望
std::format在现代C++中的地位与作用
与其他语言的格式化库的比较
C++标准化进程中格式化相关的未来发展
10.一百个std::format使用示例
1. 引言
在本文中,我们将详细讨论C++格式化的传统问题与挑战,以及C++20标准中引入std::format的背景。

传统C++格式化的问题与挑战
传统C++格式化存在一些问题与挑战,主要包括以下几点:

可读性差:使用C++中的printf和scanf家族函数进行格式化输出和输入时,它们的语法较为复杂,难以阅读。在较大的代码项目中,可读性差会导致维护困难。
类型安全性差:printf和scanf等函数无法在编译期间检查参数的类型是否正确。这可能导致运行时错误,甚至引发程序崩溃。
不够灵活:对于复杂的格式化需求,printf和scanf等函数提供的功能有限。例如,它们不支持自定义类型的格式化,也不方便处理宽字符和多字节字符集。
性能开销:由于传统的格式化方法在运行时需要处理格式字符串,它们可能导致额外的性能开销。
C++20引入std::format的背景
鉴于传统C++格式化方法的局限性,C++20标准中引入了std::format库,旨在提供一种更现代、更安全、更灵活的格式化方法。引入std::format的主要动机包括:

提高可读性:std::format采用了一种更加简洁、易懂的语法,使得格式化字符串更具可读性。
增强类型安全:std::format在编译期间就可以检查参数类型的正确性,从而降低运行时错误的风险。
扩展功能:std::format支持自定义类型的格式化,同时兼容宽字符和多字节字符集。这使得开发人员能够满足更为复杂的格式化需求。
性能优化:std::format设计时充分考虑了性能问题,相比传统的格式化方法,它在许多场景下能够提供更高的性能。
总之,std::format作为C++20标准的一部分,旨在解决传统C++格式化方法的问题,并为开发者提供一种更现代、更安全、更灵活的格式化工具。

2. std::format简介
在本节中,我们将简要介绍std::format的基本概念,并对比std::format与printf和iostreams之间的差异。

std::format的基本概念
std::format是C++20标准库中新增的一个格式化工具,它基于Python中的str.format()函数,提供了一种类型安全且易于阅读的字符串格式化方法。std::format的主要特点包括:

替换字段:std::format使用花括号{}作为替换字段的占位符。这些替换字段在格式化时会被相应的参数值替换。
格式规范:std::format支持在替换字段内部定义格式规范,例如指定输出宽度、对齐方式和填充字符等。格式规范使用冒号:分隔,放在花括号内。
编译时类型检查:std::format在编译期间检查参数类型的正确性,以提高类型安全性。
自定义类型支持:std::format可以通过重载formatter特化来支持自定义类型的格式化。
std::format与printf、iostreams的对比
下面我们将对比std::format与printf和iostreams之间的主要差异:

可读性:std::format使用花括号作为占位符,并允许在占位符内定义格式规范。这使得格式化字符串更具可读性,相较于printf和iostreams更为简洁明了。

示例:

std::cout << std::format("Hello, {}!\n", "World"); // std::format
printf("Hello, %s!\n", "World"); // printf
std::cout << "Hello, " << "World" << "!\n"; // iostreams

1
2
3
4
类型安全:std::format在编译期间检查参数类型的正确性,而printf在运行时检查类型。iostreams也具有类型安全性,但std::format更接近printf的语法,使得从printf迁移到std::format更容易。

扩展性:std::format支持自定义类型的格式化,而printf仅支持内置类型。iostreams通过重载插入和提取操作符支持自定义类型,但std::format提供更为统一的扩展方法。

性能:std::format在设计时充分考虑了性能问题,因此在许多场景下性能优于iostreams。而与printf相比,std::format的性能表现也非常出色。

综上所述,std::format在可读性、类型安全性、扩展性和性能方面都表现优异,成为现代C++编程中推荐的字符串格式化工具。

高效使用std::format的理由
以下是为什么应该高效使用std::format的几个理由:

统一的格式化语法:std::format提供了一种统一的格式化语法,无论是内置类型还是自定义类型,都可以使用相同的方法进行格式化。这有助于简化代码并降低维护成本。
简化代码:由于std::format提供了更简洁的语法,使用它可以减少代码量,使代码更易于理解。相较于printf和iostreams,std::format更适合处理复杂的字符串格式化需求。
避免运行时错误:std::format在编译期间检查参数类型,能够减少因类型错误导致的运行时错误。这有助于提高代码的健壮性和稳定性。
易于迁移:对于已经习惯使用printf的开发者,std::format提供了类似的语法和功能,可以轻松从printf迁移到std::format。
便于调试和优化:std::format的性能表现优异,且支持各种格式化选项,方便开发者进行调试和性能优化。
总之,std::format作为C++20标准库的一部分,为开发者提供了强大、易用的字符串格式化工具。使用std::format可以简化代码、提高可读性、增强类型安全性,并有助于提高代码的健壮性和性能。因此,在现代C++编程中,高效使用std::format是非常重要的。

3. 基本用法
在本节中,我们将介绍std::format的基本用法,包括格式字符串与占位符、类型规格与格式选项的使用。

格式字符串与占位符
std::format使用格式字符串来定义输出的格式。格式字符串中的占位符用花括号{}表示,可以包含以下几个部分:

参数索引:位于花括号内的数字,用于指定要替换的参数的位置。例如,{0}表示第一个参数,{1}表示第二个参数,依此类推。
格式规范:位于冒号:之后的部分,用于指定参数的格式选项。例如,{:d}表示将参数格式化为十进制整数。
文本:花括号之间可以包含任意文本,这些文本将原样输出。例如,{0} is {1}中的is会原样输出。
以下是一些基本的例子:

#include <iostream>
#include <format>

int main() {
int age = 30;
double pi = 3.1415926;
std::string name = "Alice";

std::cout << std::format("My name is {0} and I am {1} years old.\n", name, age);
std::cout << std::format("Pi is approximately {0}.\n", pi);

return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
类型规格与格式选项
std::format支持各种类型规格与格式选项,以便对输出进行详细的控制。以下是一些常见的类型规格与格式选项:

整数:

d:十进制整数。
x:小写十六进制整数。
X:大写十六进制整数。
o:八进制整数。
b:二进制整数。
示例:
std::cout << std::format("{0:d} {0:x} {0:X} {0:o} {0:b}\n", 42);

1
2
浮点数:

f:固定点表示法。
e:小写科学计数法。
E:大写科学计数法。
g:根据值选择最简表示法(f或e)。
G:根据值选择最简表示法(f或E)。
示例:
std::cout << std::format("{0:f} {0:e} {0:E} {0:g} {0:G}\n", 3.1415926535);

1
2
字符串:

s:字符串。
示例:
std::cout << std::format("{:s}\n", "Hello, World!");

1
2
宽度、对齐和填充:

<:左对齐。
>:右对齐。
^:居中对齐。
数字:指定输出宽度。
字符:指定填充字符。
示例:
std::cout << std::format("{:<10} | {:>10} | {:^10}\n", "left", "right", "center");
std::cout << std::format("{:*<10} | {:#>10} | {:_^10}\n", "left", "right", "center");

1
2
3
精度

对于浮点数,精度用于指定小数点后的位数;对于字符串,精度用于指定最大输出长度。

示例:

std::cout << std::format("{:.2f} | {:.3e} | {:.4s}\n", 3.1415926, 12345.6789, "abcdefgh");
1
整数和浮点数的进位:

整数和浮点数的进位可以使用#选项,它会在八进制和十六进制数字前添加0或0x(0X)前缀,或在浮点数上强制输出小数点。

示例:

std::cout << std::format("{:#x} | {:#o} | {:#f}\n", 42, 42, 3.14);

1
2
正负号:

使用+选项可以强制输出正数的正号。

示例:

std::cout << std::format("{:+d} | {:+f}\n", 42, 3.14);

1
2
自定义类型

: 要格式化自定义类型,需要为类型特化std::formatter模板,并提供parse和format成员函数。这使得std::format可以以一种统一的方式处理内置类型和自定义类型。

示例:

struct Point {
int x, y;
};

template<>
struct std::formatter<Point> {
auto parse(format_parse_context& ctx) {
return ctx.begin();
}

auto format(const Point& p, format_context& ctx) {
return std::format_to(ctx.out(), "({:d}, {:d})", p.x, p.y);
}
};

std::cout << std::format("{0}\n", Point{3, 4});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
4. 格式化数字
在使用std::format时,您可能会需要更多地控制数字的格式。在本节中,我们将详细讨论数字格式化的选项,包括宽度、精度、填充、正负号显示、进制转换以及浮点数格式化选项。

控制数字的宽度、精度与填充
要控制数字的宽度,请在格式说明符中指定一个整数。此外,您还可以使用0指定填充字符,例如{:05}表示将数字格式化为至少5个字符宽,不足部分用零填充。以下是一些示例:

std::cout << std::format("{:5}", 42); // " 42"
std::cout << std::format("{:05}", 42); // "00042"

1
2
3
对于浮点数,您可以使用.后接一个整数来指定精度。例如:

std::cout << std::format("{:.2f}", 3.14159); // "3.14"

1
2
显示或隐藏正负号
要显示数字的正负号,可以使用+标志。例如:

std::cout << std::format("{:+}", 42); // "+42"
std::cout << std::format("{:+}", -42); // "-42"

1
2
3
进制转换(十进制、十六进制、八进制等)
要将数字格式化为其他进制,可以使用以下格式说明符:

d:十进制(默认)
x:十六进制(小写字母)
X:十六进制(大写字母)
o:八进制
b:二进制(小写字母)
B:二进制(大写字母)
以下是一些示例:

std::cout << std::format("{:x}", 42); // "2a"
std::cout << std::format("{:X}", 42); // "2A"
std::cout << std::format("{:o}", 42); // "52"
std::cout << std::format("{:b}", 42); // "101010"

1
2
3
4
5
浮点数格式化选项
对于浮点数,您可以使用以下格式说明符:

f:定点表示(默认)
F:定点表示(无穷大和非数字为大写表示)
e:科学计数法(小写字母)
E:科学计数法(大写字母)
g:通用格式,根据值的大小和指定精度自动选择定点表示或科学计数法(小写字母)
G:通用格式,根据值的大小和指定精度自动选择定点表示或科学计数法(大写字母)
以下是一些示例:

std::cout << std::format("{:.2F}", 42.123); // "42.12"
std::cout << std::format("{:.2e}", 42.123); // "4.21e+01"
std::cout << std::format("{:.2E}", 42.123); // "4.21E+01"
std::cout << std::format("{:.2g}", 42.123); // "42.12"
std::cout << std::format("{:.2G}", 42.123); // "42.12"
std::cout << std::format("{:.2g}", 0.000421); // "0.000421"
std::cout << std::format("{:.2G}", 0.000421); // "0.000421"


1
2
3
4
5
6
7
8
9
通过上述示例,您可以看到不同浮点数格式化选项的使用方法。这使得std::format成为一个非常灵活和强大的工具,能够处理各种数字格式化需求。请尝试根据您的应用需求进行调整和组合这些选项。

5. 格式化文本
在使用std::format时,除了处理数字之外,您还需要考虑如何格式化文本。本节将讨论如何使用std::format来处理字符串的宽度、填充、特殊字符、转义以及多语言和Unicode字符。

控制字符串的宽度与填充
要设置字符串的最小宽度,请在格式说明符中指定一个整数。您还可以通过在整数前加上填充字符来设置填充字符。以下是一些示例:

std::cout << std::format("{:10}", "hello"); // "hello "
std::cout << std::format("{:_<10}", "hello"); // "hello_____"

1
2
3
处理特殊字符与转义
要在格式化字符串中包含大括号{},请使用两个连续的大括号{{或}}进行转义。以下是一个示例:

std::cout << std::format("The set contains {{1, 2, 3}}"); // "The set contains {1, 2, 3}"

1
2
要在格式化字符串中包含反斜杠和其他特殊字符,请使用反斜杠进行转义,如\n表示换行符,\t表示制表符等。例如:

std::cout << std::format("Line 1\\nLine 2"); // "Line 1\nLine 2"

1
2
使用std::format处理多语言与Unicode
std::format支持Unicode字符和多语言文本处理。为了确保正确处理Unicode字符,请使用u8前缀表示UTF-8编码的字符串字面值。以下是一个示例:

std::cout << std::format(u8"你好,世界!"); // "你好,世界!"

1
2
在处理Unicode字符串时,确保使用正确的编码,否则可能会导致乱码或无法解释的字符。std::format兼容C++17及更高版本的std::u8string类型,允许您更轻松地处理多语言文本。

总之,std::format提供了处理字符串宽度、填充、特殊字符、转义以及多语言和Unicode字符的能力。这使得std::format成为一个非常适用于现代C++应用程序的强大工具。

6. 格式化日期与时间
std::format可以与C++的chrono库一起使用,方便地格式化日期和时间。本节将讨论如何使用std::format处理时间点、持续时间、时间格式化选项以及本地化日期和时间的显示。

使用chrono库处理时间点与持续时间
chrono库提供了表示时间点和持续时间的类,如system_clock::time_point、steady_clock::time_point、duration等。要使用std::format格式化这些类型,首先需要包含<chrono>和<format>头文件。

以下是一个示例:

#include <chrono>
#include <format>
#include <iostream>

int main() {
auto now = std::chrono::system_clock::now();
auto seconds_since_epoch = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch());
std::cout << std::format("Seconds since epoch: {}\n", seconds_since_epoch.count());
}

1
2
3
4
5
6
7
8
9
10
时间格式化选项
要格式化日期和时间,可以使用扩展的格式说明符,如下所示:

%Y:四位年份
%m:月份(01-12)
%d:月份中的第几天(01-31)
%H:小时(00-23)
%M:分钟(00-59)
%S:秒(00-60,因闰秒可能为60)
为了使用这些格式化选项,需要先将chrono中的time_point转换为std::tm结构,并包含<iomanip>头文件。以下是一个示例:

#include <chrono>
#include <format>
#include <iomanip>
#include <iostream>

int main() {
auto now = std::chrono::system_clock::now();
auto now_t = std::chrono::system_clock::to_time_t(now);
auto now_tm = *std::localtime(&now_t);

std::cout << std::format("{:%Y-%m-%d %H:%M:%S}\n", now_tm);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
本地化日期与时间的显示
要显示本地化的日期和时间,可以使用std::locale。使用imbue()函数将流与特定的语言环境关联起来。以下是一个示例:

#include <chrono>
#include <format>
#include <iomanip>
#include <iostream>
#include <locale>

int main() {
auto now = std::chrono::system_clock::now();
auto now_t = std::chrono::system_clock::to_time_t(now);
auto now_tm = *std::localtime(&now_t);

std::locale::global(std::locale(""));
std::cout.imbue(std::locale());

std::cout << std::format("{:%c}\n", now_tm);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
请注意,std::locale::global()和imbue()函数的参数取决于您的平台和语言设置。本示例设置为系统默认语言环境。您还可以为特定的流或字符串指定语言环境。

通过以上方法,您可以使用std::format来灵活地处理和格式化日期与时间。与C++的chrono库结合使用,可以更方便地处理时间点和持续时间,同时允许您定制时间格式化选项以适应不同的应用场景。同时,通过std::locale类,您还可以实现日期和时间的本地化显示,以适应不同地区的用户。

下面是一些日期和时间格式化选项的示例:

#include <chrono>
#include <format>
#include <iomanip>
#include <iostream>
#include <locale>

int main() {
auto now = std::chrono::system_clock::now();
auto now_t = std::chrono::system_clock::to_time_t(now);
auto now_tm = *std::localtime(&now_t);

std::cout << std::format("{:%A, %B %d, %Y}\n", now_tm); // 显示星期、月份、日期和年份,例如:"Sunday, April 09, 2023"

std::cout << std::format("{:%D}\n", now_tm); // 以MM/DD/YY格式显示日期,例如:"04/09/23"

std::cout << std::format("{:%T}\n", now_tm); // 以HH:MM:SS格式显示时间,例如:"17:30:59"

std::cout << std::format("{:%r}\n", now_tm); // 以12小时制显示时间,例如:"05:30:59 PM"
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
7. 自定义类型的格式化
std::format允许您为自定义类型实现格式化支持,这为您的自定义类型提供了更好的输出显示。要实现自定义类型的格式化支持,您需要特化std::formatter。在本节中,我们将讨论如何为自定义类型实现格式化输出。

实现自定义类型的格式化支持
要为自定义类型实现格式化支持,您需要为其特化std::formatter,并重载parse()和format()成员函数。

以下是实现自定义类型格式化输出的步骤:

包含<format>头文件。
为您的自定义类型特化std::formatter。
在特化的std::formatter中,重载parse()和format()成员函数。
使用fmt::formatter特化
以下是一个简单的自定义类型(Person)和std::formatter特化的示例:

#include <format>
#include <iostream>
#include <string>

struct Person {
std::string name;
int age;
};

template <>
struct std::formatter<Person> {
constexpr auto parse(format_parse_context& ctx) {
auto it = ctx.begin();
auto end = ctx.end();
if (it != end && *it != '}')
throw format_error("Invalid format");
return it;
}

auto format(const Person& p, format_context& ctx) {
return format_to(ctx.out(), "{} ({})", p.name, p.age);
}
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
示例:为自定义类型实现格式化输出
现在我们已经为Person类型实现了std::formatter特化,可以使用std::format函数轻松格式化Person对象了:

int main() {
Person alice{"Alice", 30};
std::cout << std::format("{}", alice) << std::endl; // 输出:"Alice (30)"
}

1
2
3
4
5
通过实现std::formatter特化并重载parse()和format()成员函数,您可以为自定义类型提供灵活且易于使用的格式化支持。这可以大大提高您的C++代码的可读性和维护性。

8. std::format的高级技巧与应用
在本节中,我们将讨论std::format的一些高级技巧和应用,包括动态生成格式字符串、与其他标准库组件(如容器、文件操作等)的结合使用以及提高格式化性能的建议。

格式字符串的动态生成
在某些情况下,您可能需要根据运行时参数动态生成格式字符串。可以使用std::string或其他字符串处理方法来实现这一点。例如,您可能需要根据用户输入设置小数点后的位数:

#include <format>
#include <iostream>

int main() {
double pi = 3.141592653589793;
int precision = 2;
std::string format_str = "{:." + std::to_string(precision) + "f}";
std::cout << std::format(format_str, pi) << std::endl; // 输出:"3.14"
}

1
2
3
4
5
6
7
8
9
10
使用std::format与其他标准库组件(如容器、文件操作等)
std::format可以与其他标准库组件(如容器、文件操作等)一起使用,以提供更高级的格式化功能。以下是一些示例:

与容器一起使用:
#include <format>
#include <iostream>
#include <vector>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::string result = std::format("Numbers: [");
for (const auto& num : numbers) {
result += std::format("{}, ", num);
}
result = result.substr(0, result.size() - 2) + "]";
std::cout << result << std::endl; // 输出:"Numbers: [1, 2, 3, 4, 5]"
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
文件操作一起使用:
#include <format>
#include <fstream>
#include <iostream>

int main() {
std::ofstream output_file("output.txt");
output_file << std::format("{:<10} {:>10}\n", "Name", "Score");
output_file << std::format("{:<10} {:>10}\n", "Alice", 95);
output_file << std::format("{:<10} {:>10}\n", "Bob", 80);
output_file.close();
std::cout << "Output saved to output.txt" << std::endl;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
提高格式化性能的建议
虽然std::format在很多方面都比传统的格式化方法更高效,但在某些情况下,性能仍然是一个值得关注的问题。以下是一些建议,可以帮助您提高格式化性能:

避免频繁构建和销毁格式化字符串:在循环或高频调用的函数中避免重复构建格式化字符串。考虑将格式化字符串预先计算并存储为常量或静态变量。
减少不必要的字符串连接:在可能的情况下,尽量避免使用+运算符连接字符串。可以使用std::format直接构建最终字符串,而不是分段拼接。例如,可以将多个std::format`调用替换为一个带有多个占位符的调用。
使用预分配的内存:为频繁使用的字符串分配足够的预先分配的内存,以减少内存分配和重新分配的开销。例如,您可以使用std::string::reserve()函数为字符串预留足够的空间。
避免不必要的类型转换:在可能的情况下,尽量避免在格式化之前将数据类型转换为其他类型。例如,不要在格式化之前将int转换为std::string,而是直接使用int类型的格式规范。
选择合适的容器和算法:根据具体应用场景选择合适的容器和算法,以实现最佳性能。例如,对于需要快速插入和删除元素的场景,使用std::list或std::deque而不是std::vector。
通过遵循以上建议,您可以确保在使用std::format进行格式化操作时实现最佳性能。这将有助于提高您的C++应用程序的整体性能和响应速度。

9. 结论与展望
std::format在现代C++中的地位与作用
std::format是C++20中引入的一个重要特性,它在现代C++中扮演着重要的角色。与传统的C++格式化方法相比,如printf和iostreams,std::format提供了更为强大、灵活和安全的格式化功能。它支持类型安全,易于扩展,支持自定义类型和多语言环境。std::format有助于提高代码的可读性和维护性,使得C++在格式化方面与其他现代编程语言保持同步。

与其他语言的格式化库的比较
std::format的设计受到了其他编程语言中格式化库的启发,如Python的str.format()和f-string,以及Rust的std::fmt。与这些库相比,std::format具有类似的功能和语法,同时充分利用了C++的类型系统和编译时特性,以实现最佳性能。

C++标准化进程中格式化相关的未来发展
C++标准化进程将继续发展和完善格式化功能。例如,C++23中可能会引入std::format的扩展,以提供更丰富的格式选项和本地化支持。此外,C++社区也将继续关注其他语言的发展,以确保C++在格式化方面与时俱进。

总之,std::format为C++开发者提供了一种强大且易于使用的格式化工具。它不仅带来了更好的类型安全和扩展性,还为未来的C++标准提供了一个坚实的基础。作为现代C++的一部分,std::format将继续发展和完善,为C++程序员提供更高效和灵活的格式化解决方案。

10.一百个std::format使用示例
#include <iostream>
#include <format>

int main() {
int num = 42;
double pi = 3.1415926535;
std::string str = "Hello, World!";
std::string long_str = "This is a very long string to showcase precision";

std::cout << std::format("1. {:d}\n", num); // 1. 整数十进制
std::cout << std::format("2. {:x}\n", num); // 2. 整数小写十六进制
std::cout << std::format("3. {:X}\n", num); // 3. 整数大写十六进制
std::cout << std::format("4. {:o}\n", num); // 4. 整数八进制
std::cout << std::format("5. {:b}\n", num); // 5. 整数二进制
std::cout << std::format("6. {:f}\n", pi); // 6. 浮点数固定点表示
————————————————
版权声明:本文为CSDN博主「泡沫o0」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_21438461/article/details/130148449