C++ 格式化输出的几种方法对比

1,627 阅读3分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

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 评论文章

文章中有介绍.

现代化的格式化库应该是什么样子

  1. 方便使用
  2. 用户拓展性
  3. 安全性
  4. 性能 (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 对象有两个接口 parseformat . 解析格式的时候就会用 parse , 之后把解析下来的格式存在 formatter 对象内部, 然后调用 format 函数进行输出.

在我的网络包分析库 npan 中, 我就为 MAC 地址, IPv4 地址, IPv6 地址分别实现了 formatter 类, 实现了格式化的输出. IPv6 的地址实际上涉及到了中间 0 的省略, 还是得写个几十行.

孙孟越:网络实验: 一个网络包分析器 npan15 赞同 · 1 评论文章

编译期格式检验

实际上 parseformat 这两个函数是可以分开的. 编译期先调用 parse 函数 (constexpr) 解析好格式字符串, 然后检查一下格式有没有错误. 运行的时候再次 parseformat .

相关的宏为 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::printfmt::format 都会在栈上开一个大的缓冲区 (500 字符长, 可动态扩容), 格式化写进去以后, 再复制到屏幕上/字符串中. 所以用 fmt::format 输出会比 fmt::print 多一步生成 std::string 的过程. 使用栈上内存比用堆上内存代价低很多, 可以节省一些时间. 这个其实就是一个大号的 SSO (短字符串优化).

杂项

此外 fmtlib 还有给输出加上颜色的功能 (通过特定的控制字符, 需要终端配合), 还有安全的 printf 实现, 用户定义的 operator<< 支持等功能.