本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
printf 类格式化输出
C 标准库中的 printf
类函数, 实际上是非常广泛使用的. 他们主要的问题应该是不安全 (既不类型安全, 也可能造成缓冲区的溢出), 以及无法拓展 (无法兼容用户类型).
C++ 的流式 I/O
cout
之类的做到了类型安全, 也做到了拓展性, 但使用起来比较麻烦. 而就其实现上来说, 效率也并不见得高.
使用起来麻烦是真的, 如果想输出一个浮点数并保留小数点之后 3 位, printf
只需要 %.3f
, 而 cout
需要
#include <iomanip>
#include <iostream>
int main() {
std::cout << std::setiosflags(std::ios::fixed) << std::setprecision(3) << 0.;
}
这使用起来很麻烦. 更不要说如果你想格式化 5 个参数, 就可能要输入 10 个 <<
操作符了.
关于更底层的输入输出, 我在
孙孟越:Linux 下 C/C++ 输入输出14 赞同 · 2 评论文章
文章中有介绍.
现代化的格式化库应该是什么样子
- 方便使用
- 用户拓展性
- 安全性
- 性能 (I/O 操作本身往往是性能瓶颈, 而不是在格式化这一步)
Python 里面就有这两类格式化字符串的办法.
name = "Steven"
print(f"My name is {name}.")
print("My name is {}.".format(name))
如果想要指定格式化的格式 (specs), 我们可以这样
yes_votes = 42_572_654
no_votes = 43_132_495
percentage = yes_votes / (yes_votes + no_votes)
'{:-9} YES votes {:2.2%}'.format(yes_votes, percentage)
这个地方的 :
之后的内容就告诉 Python 该怎么格式化它. 比如 2.2%
表示一个百分数, 小数点前保留 2 位, 小数点后保留 2 位.
一个现代化的格式化库, 提供的 API 就应该像 Python 这样子.
fmtlib 的设计
fmtlib 也设计了类似的 API. 我们可以写出
fmt::print("Elapsed time: {0:.2f} seconds", 1.23);
fmt::print(stderr, "Don't {}!", "panic");
这样格式化变得非常的方便了.
之前的例子, 我们就可以
fmt::print("{:.2f}",0);
和 printf
一样一行解决战斗.
对于 vector
这样的类, 其实也可以很方便地输出,
std::vector<int> v = {1, 2, 3};
fmt::print("{}", fmt::join(v, ", "));
而且这个的效率实际上也很高.
第二点是安全性, 得益于 C++ 模板的强大表达力, 我们会为不同的参数列表类型, 生成一个特定的 print
函数. 也就是说, 参数列表的类型是写进了 print
函数里面的. 这样理论上可以保证我们确定参数的列表, 给了我们一个能保证安全性的机会.
用户拓展性
实际上每次输出, print
函数都会先分析一下这个格式化串, 遇到 specs 时候会把任务交给 formatter
对象进行处理. 你可以为你的类型定义一个 formatter
对象.
formatter
对象有两个接口 parse
和 format
. 解析格式的时候就会用 parse
, 之后把解析下来的格式存在 formatter
对象内部, 然后调用 format
函数进行输出.
在我的网络包分析库 npan 中, 我就为 MAC 地址, IPv4 地址, IPv6 地址分别实现了 formatter
类, 实现了格式化的输出. IPv6 的地址实际上涉及到了中间 0 的省略, 还是得写个几十行.
孙孟越:网络实验: 一个网络包分析器 npan15 赞同 · 1 评论文章
编译期格式检验
实际上 parse
和 format
这两个函数是可以分开的. 编译期先调用 parse
函数 (constexpr) 解析好格式字符串, 然后检查一下格式有没有错误. 运行的时候再次 parse
和 format
.
相关的宏为 FMT_STRING
. 具体 API 可以参考
API Reference - fmt 7.1.3 documentationfmt.dev/latest/api.html#compile-time-format-string-checks
编译期格式解析
对于 C++17 以后, 利用 if constexpr
还可以把格式字符串在编译器处理完. 编译期先调用 parse
函数解析好格式字符串, 然后检查一下格式有没有错误, 最后把字符串 parse
的结果存进这个 print
函数, 这个 print 函数就是专属于这个格式串的了.
运行的时候进行 format
, 就省下了 parse
的时间. 不过这样的话, 需要为每个格式字符串都生成一个 print
函数, 可能会导致代码膨胀.
相关的宏为 FMT_COMPILE
. 具体 API 可以参考
API Reference - fmt 7.1.3 documentationfmt.dev/latest/api.html#format-string-compilation
输出到 string
fmtlib 主要提供了两类 API, 除了刚刚说的 fmt::print
, 还有 fmt::format
函数. fmt::format
任务就是把格式化输出到 std::string
里面.
fmt::format
基本上实现了 std::format
的大部分任务. 但标准库中没有吸收 fmt::print
, 所以标准中格式化输出给的例子是 std::cout << std::format(...)
这样的操作.
fmt::print
是比 "fmt::format
之后再输出到屏幕上" 效率高的.
目前来看 fmt::print
和 fmt::format
都会在栈上开一个大的缓冲区 (500 字符长, 可动态扩容), 格式化写进去以后, 再复制到屏幕上/字符串中. 所以用 fmt::format
输出会比 fmt::print
多一步生成 std::string
的过程. 使用栈上内存比用堆上内存代价低很多, 可以节省一些时间. 这个其实就是一个大号的 SSO (短字符串优化).
杂项
此外 fmtlib 还有给输出加上颜色的功能 (通过特定的控制字符, 需要终端配合), 还有安全的 printf
实现, 用户定义的 operator<<
支持等功能.