每周技巧 #93:使用 `absl::Span`

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #93: using absl::Span

原文最初作为 TotW #93 发布于 2015 年 4 月 23 日。

作者:Samuel Benzaquen

更新于 2023 年 5 月 8 日。

在 Google,当我们想处理不拥有所有权的字符串时,已经习惯把 string_view 用作函数参数和返回类型。它可以让 API 更灵活,并通过避免不必要的 string 转换来提升性能。(技巧 #1

string_view 有一个更通用的亲戚,叫 absl::Spanabsl/types/span.h)。注意,虽然 absl::Span 在用途上类似于 C++20 提供的 std::span,但这两种类型不能互换。

Span<const T> 之于 std::vector<T>,就像 string_view 之于 string。它为 vector 中的元素提供只读接口,但也可以从非 vector 类型(例如数组和 initializer list)构造,而且不会产生复制元素的成本。

可以去掉 const,所以 Span<const T> 是指向元素不可修改数组的视图,而 Span<T> 允许对元素进行非 const 访问。不过,与 const span 不同,这类 span 需要显式构造。

作为函数参数

Span 用作函数参数的一些好处,和使用 string_view 类似。

调用方可以传入原始 vector 的一个切片,也可以传入普通数组。它还兼容其他类似数组的容器,例如 absl::InlinedVectorabsl::FixedArraygoogle::protobuf::RepeatedField 等。

string_view 一样,Span 用作函数参数时通常最好按值传递。这种形式稍快,并且生成的代码更小。

示例:

 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
void TakesVector(const std::vector<int>& ints);
void TakesSpan(absl::Span<const int> ints);

void PassOnlyFirst3Elements() {
  std::vector<int> ints = MakeInts();
  // 我们需要创建一个临时 vector,并产生一次分配和一次拷贝。
  TakesVector(std::vector<int>(ints.begin(), ints.begin() + 3));
  // 使用 Span 时不会发生拷贝或分配。
  TakesSpan(absl::Span<const int>(ints.data(), 3));
}

void PassALiteral() {
  // 这会创建一个临时 std::vector<int>。
  TakesVector({1, 2, 3});
  // Span 不需要临时分配和拷贝,因此更快。
  TakesSpan({1, 2, 3});
}
void IHaveAnArray() {
  int values[10] = ...;
  // 再一次,这会创建临时 std::vector<int>。
  TakesVector(std::vector<int>(std::begin(values), std::end(values)));
  // 直接传入数组。Span 会自动检测大小。
  // 没有发生拷贝。
  TakesSpan(values);
}

指针 vector 的 const 正确性

到处传递 std::vector<T*> 的一个大问题是:不改变容器类型,就无法让被指对象 const。

任何接收 const std::vector<T*>& 的函数,都不能修改这个 vector,但可以修改其中的 T。返回 const std::vector<T*>& 的访问器也同样如此。你无法阻止调用方修改这些 T

常见“解决方案”包括把 vector 拷贝或强制转换成正确类型。这些方案要么很慢(拷贝),要么是未定义行为(强制转换),都应该避免。请改用 Span

示例:函数参数

考虑这些 Frob 变体:

1
2
3
void FrobFastWeak(const std::vector<Foo*>& v);
void FrobSlowStrong(const std::vector<const Foo*>& v);
void FrobFastStrong(absl::Span<const Foo* const> v);

从一个需要 frob 的 const std::vector<Foo*>& v 开始,你有两个不完美选项和一个好选项。

1
2
3
4
5
6
// 快、容易输入,但不是 const-safe。
FrobFastWeak(v);
// 慢而冗长,但安全。
FrobSlowStrong(std::vector<const Foo*>(v.begin(), v.end()));
// 快、安全且清楚!
FrobFastStrong(v);

示例:访问器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class MyClass {
 public:
  // 这本应是 const。
  // 请不要修改我的 Foos,拜托。
  const std::vector<Foo*>& shallow_foos() const { return foos_; }
  // 真正的深 const。
  absl::Span<const Foo* const> deep_foos() const { return foos_; }

 private:
  std::vector<Foo*> foos_;
};
void Caller(const MyClass* my_class) {
  // 意外违反 MyClass::shallow_foos() 的契约。
  my_class->shallow_foos()[0]->SomeNonConstOp();
  // 这一行无法编译。
  // my_class->deep_foos()[0]->SomeNonConstOp();
}

结论

使用得当时,absl::Span 可以提供解耦、const 正确性和性能收益。

需要注意的是,Span 的行为很像 string_view:它是对某些外部拥有数据的引用。所有相同警告都适用。尤其是,Span 不能比它引用的数据活得更久。

每周技巧 #90:退役的标志

上一节

每周技巧 #94:调用点可读性和 bool 参数

下一节