每周技巧 #124:`absl::StrFormat()`

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #124: absl::StrFormat()

原文最初作为 TotW #124 发布于 2016 年 10 月 11 日。

更新于 2022 年 11 月 16 日。

快捷链接:abseil.io/tips/124

str_format 库和 absl::StrFormat()

经过长期测试和开发后,我们很高兴宣布 str_format 库现在已经正式可用。str_format 是一个非常高效、类型安全且可扩展的库,实现了全部 printf 格式化语法。几乎所有 printf 风格的转换都可以轻松升级为 absl::StrFormat()。更详细的文档见 https://abseil.io/docs/cpp/guides/format。对于 printf 风格的格式化,它是最佳选择;不过本文不讨论 printf 风格在哪些地方适合或不适合。

用法很简单。添加对 //third_party/absl/strings:str_format 的 BUILD 依赖,并包含头文件:

1
#include "absl/strings/str_format.h"

大多数用户会像过去调用 StringPrintf()util::format::StringF() 一样,直接调用 absl::StrFormat() 来使用 str_format 库。此外还有 StrAppendFormat()StreamFormat() 变体。

1
std::string s = absl::StrFormat("%d %s\n", 123, "hello");

与 C 库的 printf() 不同,absl::StrFormat() 转换的正确性不依赖调用方把实参的精确类型编码进格式字符串。使用 printf() 时,必须通过长度修饰符和转换说明符小心完成这一点,例如用 %llu 表示 unsigned long long 类型。但 absl::StrFormat() 是用 C++ 写的,因此它使用模板和重载,直接安全地处理调用方实参列表中的类型。在 absl::StrFormat() 中,格式转换指定的是更宽泛的 C++ 概念类别,而不是某个精确类型。例如,%s 会绑定到任何类似字符串的实参,因此 std::stringabsl::string_viewCordconst char* 都可以接受。同样,%d 接受类似整数的实参,等等。它还可以为基本用户自定义类型进一步扩展(不过目前我们希望先由我们管理这些扩展)。它会忽略 ll 这样的长度修饰符,并格式化任何可用值。例如,在下面代码里,客户端不必不必要地硬编码 x 的数据成员类型:

1
2
  X x = project_x::GetStats();
  LOG(INFO) << absl::StreamFormat("%s:%08x", x.name, x.size);

name 可以是任何类似字符串的东西,size 可以是任何类似整数的类型。这种解耦对 project_x 的维护者非常有利。

借助 str_format 库,我们还可以更顺滑地控制输出目的地。在 printf() 家族中,fprintf() 用于 FILE* 输出,sprintf() 用于写入缓冲区,asprintf() 用于写入已分配内存,还有现在已废弃的 StringPrintf() 用法(会浪费性地多次调用 vsnprintf())。str_format 库使用抽象 sink,因此可以在不损失效率的前提下自定义目的地。作为内置能力,我们有用于生成新 std::stringabsl::StrFormat(),用于追加到 std::stringabsl::StrAppendFormat(),以及用于写入 std::ostream(例如日志)的 absl::StreamFormat()

在 clang 编译器下,字面量格式字符串会进行编译期检查。对于少见的运行时决定格式字符串的情况,格式字符串必须先解析,并根据实参列表规格检查兼容性,然后才能使用。这消除了传统 printf() 使用运行时格式时的一种危险。生成已解析格式说明符的能力(类似正则表达式可以编译成 RE 对象)可以带来性能提升,因此在性能敏感代码中,即使格式字符串是静态确定的,也可能会使用它。

它与 printf() 有一些值得注意的差异(见 https://abseil.io/docs/cpp/guides/format)。我们在 str_format 库中试图做到灵活而不丢失信息。如果有符号实参使用 %u%x 这样的无符号转换进行格式化,我们会先把实参转换为对应的无符号整数类型再格式化,因此用 %u 打印负数时,其行为可能不同于你对这个先前未定义行为的预期。

最重要的是,它经过高度优化,比 sprintf() 或已废弃的 StringPrintf() 快得多(见 format-shootout)。请在任何会使用 printf 风格格式化的地方试试这个库。

示例

最后给出几个示例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "absl/strings/str_format.h"

absl::StrAppendFormat(&s, "Also, %s\n", epilogue);

// 记录类似这样的内容:"billydonahue         12345.67"
// 格式化流式输出时,优先使用 `absl::StreamFormat()`,而不是
// `std::setw` 这样的 stream I/O 操纵器。
// 更多信息见 https://google.github.io/styleguide/cppguide.html#Streams。
for (const auto& g : hard_workers)
  LOG(INFO) << absl::StreamFormat("%-20s %8.2f", g.username, g.bonus);

// POSIX 位置说明符(生成 "veni, vidi, vici!")。
summary = absl::StrFormat("%2$s, %3$s, %1$s!", "vici", "veni", "vidi");

std::string letter = response.format_string();  // 只有运行时才知道
// 拒绝不可接受的套用信函。
auto format = absl::ParsedFormat<'d', 's', 's'>::New(letter);
if (!format) {
  // ... 错误情况 ...
  return;
}
letter = StringF(*format, vacation_days, from, to);

// 为性能预编译。生成的行例如:
//   "<tr><td>alice</td><td>00000123</td></tr>\n"
//   "<tr><td>bob</td><td>00004567</td></tr>\n"
static const auto* const pfmt = new absl::ParsedFormat<'s','d'>(
    "<tr><td>%s</td><td>%08d</td></tr>\n");
for (const auto& joe : folks) {
  absl::StrAppendFormat(&output, *pfmt, joe.name, joe.id);
}
// 'FormatStreamed' 适配器可用于格式化任何可由 ostream 格式化的 'x'。
s = absl::StrFormat("[%-12s]", absl::FormatStreamed(x));

每周技巧 #123:`absl::optional` 和 `std::unique_ptr`

上一节

每周技巧 #126:`make_unique` 是新的 `new`

下一节