每周技巧 #215:使用 `AbslStringify()` 将自定义类型字符串化

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #215: Stringifying Custom Types with AbslStringify()

原文最初作为 TotW #215 发布于 2022 年 11 月 2 日。

作者:Phoebe Liang

更新于 2022 年 11 月 16 日。

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

Abseil 现在包含一种新的轻量机制 AbslStringify(),允许用户把用户自定义类型格式化为字符串。使用 AbslStringify() 扩展的用户自定义类型,可以开箱即用于 absl::StrFormatabsl::StrCatabsl::Substitute

与大多数类型扩展一样,你应该拥有你想扩展的类型。

假设我们有一个简单的 Point 结构体:

1
2
3
4
struct Point {
  int x;
  int y;
};

如果希望 Point 可以被 absl::StrFormat()absl::StrCat()absl::Substitute() 格式化,我们添加一个名为 AbslStringify()friend 函数模板:

1
2
3
4
5
6
7
8
9
struct Point {
  template <typename Sink>
  friend void AbslStringify(Sink& sink, const Point& p) {
    absl::Format(&sink, "(%d, %d)", p.x, p.y);
  }

  int x;
  int y;
};

注意:AbslStringify() 使用一个通用 “sink” 缓冲区构造字符串。这个 sink 的接口类似于 absl::FormatSink,但不支持 PutPaddedString()

现在 absl::StrCat("The point is ", p)absl::Substitute("The point is $0", p) 会直接工作。

注意:absl::StrFormat() 还提供一个更可自定义的扩展点 AbslFormatConvert(),但它不受 absl::StrCat() 支持。

使用 %v 说明符进行类型推导

但如果我们想用 absl::StrFormat() 格式化这个类型呢?absl::StrFormat() 现有的类型说明符不支持使用 AbslStringify() 扩展的用户自定义类型,所以我们不得不写成这样:

1
absl::StrFormat("The point is (%d, %d)", p.x, p.y)

这显然不理想。它完全没有使用这个扩展,而且重复了 AbslStringify() 定义中的格式字符串。我们可以改用新的类型说明符 %v

1
absl::StrFormat("The point is %v", p)

%v 使用类型推导来格式化实参。%v 支持大多数基本类型,以及任何用 AbslStringify() 扩展的类型。你可以把 %v 看作一种泛型方式,用来格式化 absl::StrFormat() 可以推导出的任意类型的“值”。%v 也可以直接用在 AbslStringify() 定义中。

%v 说明符推导以下类型:

  • 对有符号整数值推导为 d
  • 对无符号整数值推导为 u
  • 对浮点值推导为 g
    • double
    • float
    • long double
  • 对字符串值推导为 s
    • std::string
    • absl::string_view
    • std::string_view
    • absl::Cord

注意:不支持 const char*。更多信息见下文。

一些例子:

1
2
3
4
5
absl::StrFormat("%v", std::string{"hello"})    -> "hello"
absl::StrFormat("%v", 42)    -> "42"
absl::StrFormat("%v", uint64_t{16})    -> "16"
absl::StrFormat("%v", 1.6)  -> "1.6"
absl::StrFormat("%v", true) -> "true"

有特殊处理的类型

由于期望输出格式存在歧义,%v 有意不支持 charconst char*。布尔值会打印为 "true""false",而不是 "1""0";后者是 absl::StrFormat()absl::StrCat() 在其他情况下打印布尔值的方式。

与其他库集成

AbslStringify() 在其他 Abseil 库中也有额外支持。

日志

定义了 AbslStringify() 的类型可以直接写入日志:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct Point {
  template <typename Sink>
  friend void AbslStringify(Sink& sink, const Point& p) {
    absl::Format(&sink, "(%v, %v)", p.x, p.y);
  }

  int x = 10;
  int y = 20;
};

Point p;

LOG(INFO) << p;

这段代码会在日志中产生类似下面的消息:

1
I0926 09:00:00.000000   12345 main.cc:10] (10, 20)

建议通过实现 AbslStringify() 而不是 operator<< 让自定义类型可记录日志,因为它是一种通用字符串化扩展,同时还能启用 absl::StrFormatabsl::StrCatabsl::Substitute 支持。

Protocol Buffer 类型

Protocol buffers 可以使用 AbslStringify() 格式化。由于 AbslStringify() 相比 DebugString() 提供了整体更顺滑的用户体验,建议用户在把 proto 格式化为字符串时使用 AbslStringify()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
message MyProto {
  optional string my_string = 1
}

MyProto my_proto;
my_proto.set_my_string("hello world");

absl::StrCat("My proto is: ", my_proto);
absl::StrFormat("My proto is: %v", my_proto);
LOG(INFO) << my_proto;

结束语

除了需要它的用户自定义类型之外,%v 类型说明符旨在用于那些精确格式并不重要的场景。它本质上是一个兜底的“以人类可读方式打印它”说明符。如果你需要除此之外的任何其他保证,请使用更具体的类型说明符。

每周技巧 #198:标签类型

上一节

每周技巧 #218:使用 FTADLE 设计扩展点

下一节