查看原文
其他

说说 C++20 的格式化库

CPP开发者 2023-07-27

The following article is from CPP编程客 Author 里缪

该标准库来自开源库fmtlib,作者为Victor Zverovich,提案为P0645R10。
目前为止,仍旧只有MSVC16.10+对该库支持稍微完整,因此可以先使用fmtlib,主页为https://fmt.dev/latest/index.html。

格式化函数

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,20222022
-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, 105);

输出如下:

 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{ 12 };
2std::cout << std::format("{:f}\n", x);
3std::cout << std::format("{:e}\n", x);

输出将为:

(1.02.0)
(1.0e+002.0e+00)

通过这种方式,你可以为任何自定义类型编写合适的格式化形式。

最后,总结一下,本篇介绍了C++20格式化库的基本使用方式,这个东西其实非常强大,能够以强大的语法规则轻松实现各种各样的格式化形式,也可以为自定义类型装配格式化功能,可以说是C++20中比较常用的一个组件了。

- EOF -

推荐阅读  点击标题可跳转

1、防御性编程技巧

2、C++ 的全链路追踪方案,稍微有点高端

3、2022 技术趋势:C++、Go、Rust 大放异彩


关注『CPP开发者』

看精选C/C++技术文章 

点赞和在看就是最大的支持❤️

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存