Tip of the Week#93: 使用absl::Span

在google,当我们想处理没有owner的字符串时,通常会使用absl::string_view作为函数的参数和返回值。它能够使API更加灵活,并且它能够通过避免对std::string进行不必要的转换来提升性能。

absl::string_view有一个更加通用的表亲,被称为absl::Spanabsl::Spanstd::vector就像absl::string_viewstd::string一样。它为vector的元素提供只读接口,但是它也可以从由非vector(如数组和初始化列表)来构造,并且不会产生拷贝元素的消耗。

const可以被删除,因此absl::Span是一个元素不能改变的数组的视图,absl::Span允许对元素进行非常量访问。然而,与const的跨度不同,这些需要显式构造。

关于std::span/gsl::span的注释

需要重点注意的是,虽然absl::Span在设计和目的上与std::span方案(以及现存的gsl::span引用实现)相似,但是absl::Span目前并不保证是一个对最终标准的随时替代品,因为std::span方案仍在开发和变化。

相反,absl::Span旨在拥有一个尽可能与absl::string_view类似的接口,而不是针对特定于字符串的功能。

作为函数参数

使用absl::Span作为函数参数的一些好处类似于使用absl::string_view的好处。

调用者可以传递出事vector的一个切片,或者传递一个纯数组。它也兼容其他类数组的容器,像absl::InlinedVectorabsl::FixedArraygoogle::protobuf::RepeatedField等等。

absl::string_view一样,当用作函数参数时,通常最好是按值传递absl::Span;这种方式比通过const引用(在大多数平台上)传递稍微快点,并且生成更小的代码。

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

void PassOnlyFirst3Elements() {
std::vector<int> ints = MakeInts();
// 我们需要创建一个临时的vecotr,接着引起一个分配和拷贝
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);
}

预防缓冲区溢出

因为absl::Span知道它自己的长度,相较于C风格的指针长度,API使用它会更安全。

示例:更安全的memcpy()

1
2
3
4
5
6
// 糟糕的代码
void BadUser() {
int src[] = {1, 2, 3};
int dest[2];
memcpy(dest, src, ABSL_ARRAYSIZE(src) * sizeof(int)); // 哎呀,目标溢出.
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 一个简单的示例,但是复用Span已知的大小来阻止上述错误
template <typename T>
bool SaferMemCpy(absl::Span<T> dest, absl::Span<const T> src) {
if (src.size() > dest.size()) {
return false;
}
memcpy(dest.data(), src.data(), src.size() * sizeof(T));
return true;
}

void GoodUser() {
int src[] = {1, 2, 3}, dest[2];
// 没有溢出!
SaferMemCpy(absl::MakeSpan(dest), absl::Span<const int>(src));
}

关于指针的vectorconst的正常性

传递std::vector<T*>的一个大问题是你不能再不改变容器类型的情况下使得指针指向的内容为const

任何带有const std::vector<T*>的函数都不能修改vector,但是它能够修改T类型的值。这也适用于返回const std::vector<T*>&的访问器。你不能阻止调用者修改T类型的值。

通常的“解决方案”包括拷贝或者转换vector为正确的类型。这些解决方案是慢的(对于拷贝)或者未定义的行为(对于转换),应该避免它们。相反,请使用absl::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);

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

1
2
3
4
5
6
// 更快更容易输入但是不安全
FrobFastWeak(v);
// 更慢更混乱,但是更安全
FrobSlowStrong(std::vector<const Foo*>(v.begin(), v.end()));
// fast, safe, and clear!
FrobFastStrong(v);

示例:访问器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 糟糕的代码
class DontDoThis {
public:
// 不要修改我的Foos,拜托了。
const std::vector<Foo*>& shallow_foos() const { return foos_; }

private:
std::vector<Foo*> foos_;
};

void Caller(const DontDoThis& my_class) {
// Modifies a foo even though my_class is a reference-to-const
my_class->foos()[0]->SomeNonConstOp();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 好的代码
class DoThisInstead {
public:
absl::Span<const Foo* const> foos() const { return foos_; }

private:
std::vector<Foo*> foos_;
};

void Caller(const DoThisInstead& my_class) {
// 这个不能编译
// my_class.foos()[0]->SomeNonConstOp();
}

结论

在恰当使用时,absl::Span可以提供解耦,常量正确性和性能优势。

需要重点注意的是,absl::Span的行为非常像absl::string_view,在引用一些外部占有的数据上。所有相同的警告都适用。特别的是,absl::Span不能比它引用的数据寿命更长。