说说 C++20 的格式化库
The following article is from CPP编程客 Author 里缪
格式化函数
C++20提供了三个格式化函数,std::format(),std::format_to()和std::format_to_n()。
通过一个简单的例子来了解其用法:
1// format
2std::cout << std::format("HAPPY NYE {} EVERYONE!", 2022) << '\n';
3
4// format_to
5std::string buffer;
6std::format_to(
7 std::back_inserter(buffer),
8 "HAPPY NYE {} EVERYONE!", 2022
9);
10std::cout << buffer << '\n';
11
12// format_to_n
13buffer.clear();
14std::format_to_n(
15 std::back_inserter(buffer), 6,
16 "HAPPY NYE {} EVERYONE!", 2022
17);
18std::cout << buffer << '\n';
输出如下:
HAPPY NYE 2022 EVERYONE!
HAPPY NYE 2022 EVERYONE!
HAPPY
format系列函数都包含一个格式化串参数,其中用"{}"表示占位,具体参数在该参数之后依次指定。
std::format会返回一个std::string,所以可以通过cout直接输出格式化之后的字符串。
而std::format_to和std::format_to_n则需要指定格式化之后字符串的输出位置,后者还需指定截取的字符长度。
例子中指定了输出位置为std::string,截取长度为6,所以有了如上输出。
在std::format和std::format_to内部则使用了std::vformat和std::vformat_to,实现如下:
1template <class... _Types>
2string format(const string_view _Fmt, const _Types&... _Args) {
3 return vformat(_Fmt, make_format_args(_Args...));
4}
5
6template <output_iterator<const char&> _OutputIt, class... _Types>
7_OutputIt format_to(_OutputIt _Out, const string_view _Fmt, const _Types&... _Args) {
8 _Fmt_iterator_buffer<_OutputIt, char> _Buf(move(_Out));
9 vformat_to(_Fmt_it{_Buf}, _Fmt, make_format_args(_Args...));
10 return _Buf._Out();
11}
因而在前者无法使用的情况下,可以使用后者代替前者。[见后文]
格式化语法规范
可以在格式串参数占位符"{}"中指定更多的规则,以产生更强大的字符串格式化能力,本节展示一些常用的语法。
总的语法规范官方是这样写的:
fill-and-align(optional) sign(optional) #(optional) 0(optional) width(optional) precision(optional) L(optional) type(optional)
因此,本节就按照这个顺序分成几点来介绍。
基本语法
上节的例子还可以这样写:
1std::cout << std::format("{} {} {} {}!\n", "HAPPY", "NYE", 2022, "EVERYONE");
2std::cout << std::format("{} {} {} {}!\n", "HAPPY", "NYE", 2022, "EVERYONE", "unused");
3std::cout << std::format("{2} {1} {3} {0}!\n", "EVERYONE", "NYE", "HAPPY", 2022);
此处有几个注意点。
首先,面对不同类型,占位符无需指定具体的类型,会自动识别。
其次,若实际参数个数多于占位符个数,则会忽略多余的参数。
最后,默认参数ID是从0依次增加,可以通过显式指定参数ID来改变默认的参数顺序。
填充与对齐
其实这个语法很简单,"<"、">"、"^"三个符号分别表示左对齐、右对齐和居中对齐。整数和浮点数默认是右对齐,非整数和浮点数默认是左对齐。
看如下例子:
1int NYE = 2022;
2std::cout << std::format("{:10}", NYE) << '\n';
3std::cout << std::format("{:10}", ":)") << '\n';
4std::cout << std::format("{:*<10}", ":)") << '\n';
5std::cout << std::format("{:*>10}", ":)") << '\n';
6std::cout << std::format("{:*^10}", ":)") << '\n';
7std::cout << std::format("{:10}", true) << '\n';
将会输出:
2022
:)
:)********
********:)
****:)****
true
其中,用":"表示后面的是可选参数,"10"表示宽度,"*"表示填充的字符。
是不是感觉有点像是在写正则表达式了呀哈哈:D
sign、#和0
这三个可选规则是针对数值的。
sign用于指定正负数的符号,"+"指定在格式化后正数前面加"+"号,"-"指定负数前面加"-"号。如果是空格,则格式化后,正数前面会留个空格,负数前面则是"-"号。
#可以指定一些可替换的形式,主要是针对进制数的,如指定十六进制,则格式化后会在数值前面加"0x",二进制加"0b"。
0则会在数值前面加0,如"123"可能会变成"00123"。
例子如下:
1std::cout << std::format("{0:},{0:+},{0:-},{0: }", NYE) << '\n';
2std::cout << std::format("{0:},{0:+},{0:-},{0: }", -NYE) << '\n';
3
4std::cout << std::format("{:#010d}", NYE) << '\n'; // 十进制
5std::cout << std::format("{:#010b}", NYE) << '\n'; // 二进制
6std::cout << std::format("{:#010o}", NYE) << '\n'; // 八进制
7std::cout << std::format("{:#010x}", NYE) << '\n'; // 十六进制
8std::cout << std::format("{:<010}", NYE) << '\n'; // 指定对齐,则补0忽略
将会输出:
2022,+2022,2022, 2022
-2022,-2022,-2022,-2022
0000002022
0b11111100110
0000003746
0x000007e6
2022
值得一提的是,对齐与补0不能共存,当同时指定时,补0将会被忽略。
宽度与精度
宽度与精度主要是针对浮点数的,直接看例子:
1float NYED = 20.22f;
2std::cout << std::format("{:10f}\n", NYED);
3std::cout << std::format("{:{}f}\n", NYED, 10);
4std::cout << std::format("{:.5f}\n", NYED);
5std::cout << std::format("{:.{}f}\n", NYED, 5);
6std::cout << std::format("{:10.5f}\n", NYED);
7std::cout << std::format("{:{}.{}f}\n", NYED, 10, 5);
输出如下:
20.219999
20.219999
20.22000
20.22000
20.22000
20.22000
例子中的"10"是指定的宽度,".5"表示精度。可以直接在格式串中指定,也可以通过一个称为「内嵌替换域」的方式在参数后面指定,语法就是再格式串内容再嵌入"{}"。
自定义类型
std::format并不支持所有类型的格式化操作,如何为其增加新的类型?便需要借助自定义类型。
自定义类型需要偏特化std::formatter,然后重写parse()和format()函数。
简而言之,自定义类型需要完成两部分工作,一是解析规则,二是格式输出。
规则就是前面写的"{:}"此类语法,由于需要自己编写解析函数,所以其实可以自定义规则。格式输出就是自己决定自定义类型输出的形式,自己指定输出哪些成员变量,添加、替换或删除哪些字符等等。
这里将提供的例子来自于fmtlib的示例,我将它用C++20标准的写法进行了改写。用此示例,是因为这个例子逻辑清晰,结构简明,很适合用来学习。
示例代码如下:
1struct Point {
2 double x, y;
3};
4
5template<>
6struct std::formatter<Point> {
7 constexpr auto parse(format_parse_context& ctx) {
8 auto it = ctx.begin(), end = ctx.end();
9 if (it != end && (*it == 'f' || *it == 'e')) presentation = *it++;
10 if (it != end && *it != '}') throw std::format_error("invalid format");
11 return it;
12 }
13
14 template<typename FormatContext>
15 auto format(const Point& p, FormatContext& ctx) {
16 return presentation == 'f'
17 ? std::format_to(ctx.out(), "({:.1f}, {:.1f})", p.x, p.y)
18 : std::format_to(ctx.out(), "({:.1e}, {:.1e})", p.x, p.y);
19 }
20
21 char presentation = 'f';
22};
这个代码完全正确,但MSVC编译不过,会报:C2039"resize": 不是 "std::_Fmt_buffer<char>"的成员错误,这是MSVC的BUG,目前还没有修复。
但是,可以通过使用std::vformat_to来代替std::format_to,从而避免该错误。
于是将format()实现更改如下:
1template<typename FormatContext>
2auto format(const Point& p, FormatContext& ctx) {
3 return presentation == 'f'
4 ? std::vformat_to(ctx.out(), "({:.1f}, {:.1f})", std::make_format_args(p.x, p.y))
5 : std::vformat_to(ctx.out(), "({:.1e}, {:.1e})", std::make_format_args(p.x, p.y));
6}
现在来说parse函数,在这里解析规则。由于Point是浮点数,所以这里自定义规则为浮点表示和科学计数法表示两种形式。也就是说,规则可以为"{:f}"或"{:e}"。
parse_parse_context是解析的上下文语境,其begin()指向"{:"之后的字符,end()指向"}"。我们需要完成的工作就是解析其间的自定义规则。
在例子中,正确的规则只能是"{:f}"或"{:e}",因此判断了第一个字符是否为其中之一。迭代器向后走一位,就是"}",如果不是则表示规则错误,于是抛出异常。
format()的工作就是根据解析出来的规则,使用std::vformat_to将自定义类型欲输出内容输出到FormatContext中。这样就可以格式化自定义类型的输出形式。
完成上述操作,现在便可以使用std::format格式化自定义类型:
1Point x{ 1, 2 };
2std::cout << std::format("{:f}\n", x);
3std::cout << std::format("{:e}\n", x);
输出将为:
(1.0, 2.0)
(1.0e+00, 2.0e+00)
通过这种方式,你可以为任何自定义类型编写合适的格式化形式。
最后,总结一下,本篇介绍了C++20格式化库的基本使用方式,这个东西其实非常强大,能够以强大的语法规则轻松实现各种各样的格式化形式,也可以为自定义类型装配格式化功能,可以说是C++20中比较常用的一个组件了。
- EOF -
1、防御性编程技巧
关注『CPP开发者』
看精选C/C++技术文章
点赞和在看就是最大的支持❤️