每周技巧 #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::Span(absl/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::InlinedVector、absl::FixedArray、google::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 不能比它引用的数据活得更久。
每周技巧 #94:调用点可读性和 bool 参数
下一节