什么是 "span" 以及我什么时候应该使用它?
What is a "span" and when should I use one?
最近我收到了在我的代码中使用 span<T>
的建议,或者在网站上看到了一些使用 span
的答案——据说是某种容器。但是——我在 C++17 标准库中找不到类似的东西。
那么这个神秘的东西是什么 span<T>
,如果它不是标准的,为什么(或什么时候)使用它是个好主意?
这是什么?
一个span<T>
是:
- 内存中某处
T
类型的连续值序列的非常轻量级抽象。 - 基本上是一个
struct { T * ptr; std::size_t length; }
和一堆方便的方法。 - A non-owning 类型(即 "reference-type" 而不是“值类型”):它从不分配或取消分配任何东西,也不会使智能指针保持活动状态。
它以前被称为 array_view
and even earlier as array_ref
。
我应该什么时候使用它?
首先,当不使用它时:
- 不要在可以接受任何一对开始和结束迭代器的代码中使用它,例如
std::sort
、std::find_if
、std::copy
和所有这些 super-generic 模板函数。 - 如果您有一个您知道适合您的代码的标准库容器(或 Boost 容器等),请不要使用它。它无意取代其中任何一个。
现在实际使用它的时间:
Use
span<T>
(respectively,span<const T>
) instead of a free-standingT*
(respectivelyconst T*
) when the allocated length or size also matter. So, replace functions like:void read_into(int* buffer, size_t buffer_size);
with:
void read_into(span<int> buffer);
我为什么要使用它?为什么是好事?
哦,跨度太棒了!使用 span
...
意味着您可以使用该指针+长度/开始+结束指针组合,就像您使用花哨的 pimped-out 标准库容器一样,例如:
for (auto& x : my_span) { /* do stuff */ }
std::find_if(my_span.cbegin(), my_span.cend(), some_predicate);
std::ranges::find_if(my_span, some_predicate);
(在 C++20 中)
...但是绝对 none 的开销大多数容器 类 招致。
有时让编译器为您做更多的工作。例如,这个:
int buffer[BUFFER_SIZE]; read_into(buffer, BUFFER_SIZE);
变成这样:
int buffer[BUFFER_SIZE]; read_into(buffer);
... 这将执行您希望它执行的操作。另见 Guideline P.5.
当您希望数据在内存中连续时,是将
const vector<T>&
传递给函数的合理替代方法。不再被 high-and-mighty C++ 大师骂!促进静态分析,因此编译器可能能够帮助您捕获愚蠢的错误。
允许 debug-compilation 运行时检测 bounds-checking(即
span
的方法将在#ifndef NDEBUG
中包含一些 bounds-checking 代码...#endif
)表示您的代码(使用跨度)不拥有 pointed-to 内存。
使用 span
的动力更大,您可以在 C++ core guidelines 中找到它 - 但您会发现其中的不足。
但是它在标准库中吗?
编辑: 是的,std::span
已通过 C++20 版本的语言添加到 C++ 中!
为什么只在 C++20 中?好吧,虽然这个想法并不新鲜 - 它目前的形式是与 C++ core guidelines 项目一起构思的,该项目在 2015 年才开始成形。所以花了一段时间。
如果我正在编写 C++17 或更早版本,我该如何使用它?
它是 Core Guidelines 支持库 (GSL) 的一部分。实现:
- Microsoft / Neil Macintosh 的 GSL contains a standalone implementation:
gsl/span
- GSL-Lite 是整个 GSL 的 single-header 实现(没那么大,不用担心),包括
span<T>
.
GSL 实现通常假设平台实现了 C++14 支持 [11]。这些替代 single-header 实现不依赖于 GSL 设施:
martinmoene/span-lite
需要 C++98 或更高版本tcbrindle/span
需要 C++11 或更高版本
请注意,这些不同的 span 实现在 methods/support 附带的功能方面存在一些差异;它们也可能与 C++20 中标准库采用的版本有所不同。
进一步阅读:您可以在C++17之前的最终官方提案中找到所有细节和设计注意事项,P0122R7:span: bounds-safe views for sequences of objects by Neal Macintosh and Stephan J. Lavavej. It's a bit long though. Also, in C++20, the span comparison semantics changed (following this short paper by Tony van Eerd) .
一个span<T>
是这样的:
template <typename T>
struct span
{
T * ptr_to_array; // pointer to a contiguous C-style array of data
// (which memory is NOT allocated or deallocated
// by the span)
std::size_t length; // number of elements of type `T` in the array
// Plus a bunch of constructors and convenience accessor methods here
}
它是 light-weight 对 C-style 数组的包装器,C++ 开发人员在使用 C 库并希望使用 C++ 样式的数据容器包装它们以实现“类型安全”时首选和“C++-ishness”和“feelgoodery”。 :)
注意:我将上面定义的结构容器称为 span,一个“light-weight 对 C-style 数组的包装器”,因为它指向一个连续的片段内存,例如 C-style 数组,并用访问器方法和数组大小包装它。这就是我所说的“light-weight 包装器”的意思:它是一个指针和长度变量以及函数的包装器。
更进一步:
@einpoklum 很好地介绍了 span
是什么 in his answer here。但是,即使在阅读了他的回答后, 新手也很容易遇到一系列 stream-of-thought 未完全回答的问题,例如:
span
与 C 数组有何不同?为什么不只使用其中之一呢?好像只是其中一个尺寸也已知的...- 等等,这听起来像
std::array
,span
和那个有什么不同? - 哦,这让我想起了,
std::vector
不也和std::array
一样吗? - 我很困惑。 :( 什么是
span
?
因此,这里有一些额外的说明:
直接引用他的回答——我的补充和加粗的括号注释和我的强调用斜体:
What is it?
A
span<T>
is:
- A very lightweight abstraction of a contiguous sequence of values of type
T
somewhere in memory.- Basically a single struct
{ T * ptr; std::size_t length; }
with a bunch of convenience methods. (Notice this is distinctly different fromstd::array<>
because aspan
enables convenience accessor methods, comparable tostd::array
, via a pointer to typeT
and length (number of elements) of typeT
, whereasstd::array
is an actual container which holds one or more values of typeT
.)- A non-owning type (i.e. a "reference-type" rather than a "value type"): It never allocates nor deallocates anything and does not keep smart pointers alive.
It was formerly known as an
array_view
and even earlier asarray_ref
.
那些加粗的部分对理解来说至关重要,所以不要错过或误读它们! span
不是结构的 C-array,也不是 T
类型的 C-array 加上数组长度的结构(这本质上就是 std::array
container 是),也不是 C-array 类型为 T
的指针结构加上长度,而是 single 结构包含一个指向类型 T
的单个 指针和 length,即 T
类型的指针指向的连续内存块中元素(T
类型)的数量! 这样,您添加的唯一开销是使用 span
是存储指针和长度的变量,以及您使用 span
提供的任何便利访问器函数。
这是 UNLIKE a std::array<>
因为 std::array<>
实际上为整个连续块分配内存,它是 UNLIKE std::vector<>
因为 std::vector
基本上只是一个std::array
每次它填满并且您尝试向其中添加其他内容时,它也会 动态增长 (通常大小加倍)。 A std::array
是固定大小的,而 a span
甚至不管理它指向的块的内存,它只是指向到内存块,知道内存块有多长,知道内存中 C-array 中的数据类型,并提供方便的访问器函数来处理该连续内存中的元素 .
它是 C++标准的一部分:
从 C++20 开始,std::span
是 C++ 标准的一部分。您可以在此处阅读其文档:https://en.cppreference.com/w/cpp/container/span。要了解如何在 C++11 或更高版本 中使用 Google 的 absl::Span<T>(array, length)
,请参阅下文。
摘要说明和主要参考文献:
std::span<T, Extent>
(Extent
= "序列中元素的数量,或者std::dynamic_extent
如果是动态的。一个跨度 指向 内存并使其易于访问,但不管理它!):- https://en.cppreference.com/w/cpp/container/span
std::array<T, N>
(注意它有一个固定大小N
!):- https://en.cppreference.com/w/cpp/container/array
- http://www.cplusplus.com/reference/array/array/
std::vector<T>
(根据需要自动动态增大大小):- https://en.cppreference.com/w/cpp/container/vector
- http://www.cplusplus.com/reference/vector/vector/
如何在 C++11 或更高版本中使用 span
今天?
Google 有 open-sourced 其内部 C++11 库,其形式为“Abseil”库。该库旨在提供 C++14 到 C++20 以及适用于 C++11 及更高版本的功能,以便您今天就可以使用明天的功能。他们说:
Compatibility with the C++ Standard
Google has developed many abstractions that either match or closely match features incorporated into C++14, C++17, and beyond. Using the Abseil versions of these abstractions allows you to access these features now, even if your code is not yet ready for life in a post C++11 world.
以下是一些关键资源和链接:
- 主站点:https://abseil.io/
- https://abseil.io/docs/cpp/
- GitHub 存储库:https://github.com/abseil/abseil-cpp
span.h
header 和absl::Span<T>(array, length)
模板 class: https://github.com/abseil/abseil-cpp/blob/master/absl/types/span.h#L153
其他参考资料:
- Struct with template variables in C++
- Wikipedia: C++ classes
- default visibility of C++ class/struct members
相关:
- [我关于模板和跨度的另一个回答]How to make span of spans