什么是“span”;什么时候用?

最近我得到了在我的代码中使用span<T>的建议,或者在网站上看到了一些使用span的答案-应该是某种容器。但是-我在c++ 17标准库中找不到任何类似的东西。

那么这个神秘的span<T>是什么,为什么(或什么时候)使用它是一个好主意,如果它是非标准的?

128739 次浏览

是什么?

span<T>是:

  • 对内存中某个地方类型为T的连续值序列的非常轻量级抽象。
  • 基本上是一个struct { T * ptr; std::size_t length; }和一堆方便的方法。
  • 非拥有类型(即“reference-type"而不是“值类型”):它从不分配或释放任何东西,也不保持智能指针处于活动状态。

它以前被称为array_view,甚至更早的是array_ref

什么时候使用?

首先,当使用span时:

  • 不要在代码中使用span,因为它可以取任意一对start &结束迭代器(如std::sortstd::find_ifstd::copy和其他来自<algorithm>的模板函数),并且也不在接受任意范围的代码中(关于这些,请参阅c++ 20范围库提供信息)。span比一对迭代器或range有更严格的要求:元素的相邻性和元素在内存中的存在性。
  • 如果你有一个标准库容器(或Boost容器等),你知道它适合你的代码,就不要使用span。span并不打算取代现有的容器。

现在来看看什么时候使用span:

当分配的长度或大小也很重要时,使用span<T>(分别为span<const T>)而不是独立的T*(分别为const T*)。因此,替换函数如下:

void read_into(int* buffer, size_t buffer_size);

:

void read_into(span<int> buffer);

我为什么要用它?为什么这是一件好事?

哦,跨度太棒了!使用span…

  • 意味着你可以像使用花哨的标准库容器一样使用指针+长度/开始+结束指针组合,例如:

    • 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); (in c++ 20)

    …但是大多数容器类都不会产生任何开销。

  • 有时会让编译器为你做更多的工作。例如,这个:

    int buffer[BUFFER_SIZE];
    read_into(buffer, BUFFER_SIZE);
    

    变成这样:

    int buffer[BUFFER_SIZE];
    read_into(buffer);
    

    ... 它会做你想做的事情。另见指南第3

  • 当你希望你的数据在内存中是连续的,

    是将const vector<T>&传递给函数的合理选择。再也不会被高高在上的c++大师责骂了!

  • 有助于静态分析,因此编译器可能能够帮助您捕获愚蠢的错误。

  • 允许用于运行时边界检查的调试编译插装(即span的方法将在#ifndef NDEBUG中有一些边界检查代码…#endif)

  • 表示您的代码(使用span)不拥有指向内存。

使用__abc0甚至还有更多的动机,你可以在c++核心指南中找到它——但你明白其中的意思。

但它在标准库中吗?

是的,在c++ 20版本中,std::span被添加到c++中!

为什么只有c++ 20?好吧,虽然这个想法并不新鲜——它目前的形式是与c++核心指南项目结合起来构思的,该项目在2015年才开始成型。所以花了一段时间。

那么,如果我正在编写c++ 17或更早的版本,该如何使用它呢?

它是核心指导方针的支持库(GSL)的一部分。实现:

  • Microsoft / Neil Macintosh的GSL包含一个独立的实现:gsl/span
  • GSL-Lite是整个GSL的单头实现(别担心,它没有那么大),包括span<T>

GSL实现通常假设一个实现c++ 14支持的平台[12]。这些可选的单头实现不依赖于GSL设施:

注意,这些不同的span实现在它们所附带的方法/支持函数上有一些差异;它们也可能与c++ 20标准库中采用的版本有所不同。


进一步阅读:你可以在Neal Macintosh和Stephan J. Lavavej在c++ 17之前的最终官方提案P0122R7: Span:对象序列的边界安全视图中找到所有的细节和设计考虑因素。不过有点长。此外,在c++ 20中,span比较语义发生了变化(继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 nor deallocated
// nor in any way managed 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
}

它是C风格数组的轻量级包装器,是c++开发人员在使用C库并希望用c++风格的数据容器包装它们以“类型安全”的首选。和“c++ -ishness"和“;feelgoodery"。:)

注意:我把上面定义的结构容器称为span,称为“c风格数组的轻量级包装器”。因为它指向一个连续的内存块,比如c风格的数组,并用访问方法和数组的大小来包装它。这就是我所说的“轻量级包装器”:它是一个包含指针和长度变量以及函数的包装器。

然而,与std::vector<>和其他c++标准容器不同的是,它们也可能只有固定的类大小,并包含指向其存储内存的指针,span不拥有它所指向的内存,并且永远不会删除它,调整它的大小,也不会自动分配新内存。同样,像vector这样的容器拥有它所指向的内存,并将对其进行管理(分配、重新分配等),但span不拥有它所指向的内存,因此也不会管理它。


进一步:

@einpoklum很好地介绍了span在他的回答中。然而,即使读完了他的回答,很容易让一个新跨越的人仍然有一系列没有完全回答的思路问题,比如下面这些:

  1. span与C数组有什么不同?为什么不直接用一个呢?它似乎只是那些众所周知的大小之一……
  2. 等等,这听起来像std::arrayspan和它有什么不同?
  3. 哦,这提醒了我,std::vector不也像std::array吗?
  4. 我很困惑。span是什么?

所以,这里有一些更明确的解释:

直接引用他的答案-与MY ADDITIONS and括号注释IN BOLD我用斜体字强调:

是什么?

span<T>是:

  • 内存中某个地方T类型的连续的值序列的非常轻量级抽象。
  • 基本上是一个带有一堆方便方法的结构体{ T * ptr; std::size_t length; }(注意这与__ABC1明显不同,因为__ABC2通过指向类型T指针和类型T的长度(元素数量)来实现方便的访问方法,与__ABC3类似,而__ABC3是一个实际的容器,它保存类型T的一个或多个。)
  • non-owning类型(即“reference-type"而不是"值类型"):它永远不要分配或分配任何东西并且不保持智能指针活动。

它以前被称为array_view,甚至更早的是array_ref

这些粗体部分是至关重要的的理解,所以不要错过他们或误读他们!span不是c数组结构,也不是一个struct类型T + c数组的数组的长度(这将基本上std::array T0),也不是c数组的结构体类型的指针T +长度,而是T1结构包含一个单一T2, T3,即T4这样,唯一的开销你已经添加了通过使用一个span变量来存储指针和长度,以及span提供的任何方便的访问函数。

它不同于std::array<>,因为std::array<>实际上为整个连续块分配内存;它不同于std::vector<>,因为std::vector基本上只是一个std::array,每次它被填满并且你试图向它添加其他东西时,它也会执行动态增长(通常是大小的两倍)。std::array的大小是固定的,而a span甚至不管理它所指向的块的内存,它只是指向内存块,知道内存块有多长,知道内存中的c数组中的数据类型,并提供方便的访问函数来处理相邻内存中的元素的大小是固定的。

c++标准的一部分:

std::span是c++ 20标准的一部分。你可以在这里阅读它的文档:https://en.cppreference.com/w/cpp/container/span。要了解如何在c++ 11或更高版本的今天中使用谷歌的absl::Span<T>(array, length),请参见下面。

摘要描述和主要参考文献:

  1. std::span<T, Extent> (Extent = "序列中元素的数量,或std::dynamic_extent如果是动态的;;span只是指出内存,使它易于访问,但不管理它!):
  2. https://en.cppreference.com/w/cpp/container/span
  3. std::array<T, N>(注意它有一个固定大小N!):
  4. https://en.cppreference.com/w/cpp/container/array
  5. http://www.cplusplus.com/reference/array/array/
  6. std::vector<T>(根据需要自动动态增长大小):
  7. https://en.cppreference.com/w/cpp/container/vector
  8. http://www.cplusplus.com/reference/vector/vector/

如何在c++ 11或以后的今天中使用span ?

谷歌以“abseil”的形式开放了他们的内部c++ 11库。图书馆。这个库旨在提供c++ 14到c++ 20及以上版本的c++ 11及以后版本的特性,这样您就可以在今天使用明天的特性。他们说:

与c++标准的兼容性

谷歌开发了许多抽象,这些抽象要么与c++ 14、c++ 17以及更高版本中的特性相匹配,要么与它们紧密匹配。使用这些抽象的Abseil版本允许您现在就访问这些特性,即使您的代码还没有准备好适应后c++ 11的世界。

以下是一些关键资源和链接:

  1. 主要站点:https://abseil.io/
  2. < a href = " https://abseil。Io /docs/cpp/" rel="nofollow noreferrer">https://abseil.io/docs/cpp/
  3. GitHub仓库:https://github.com/abseil/abseil-cpp
  4. span.h头文件和absl::Span<T>(array, length)模板类:https://github.com/abseil/abseil-cpp/blob/master/absl/types/span.h#L153

其他参考资料:

  1. 结构与模板变量在c++
  2. Wikipedia: c++类 .
  3. c++类/结构成员的默认可见性

相关:

  1. [另一个关于模板和跨度的答案]如何制作网架的网架

Einpoklum提供的答案很好,但我不得不深入评论部分来理解一个具体的细节,所以这意味着作为一个扩展来澄清这个细节。

首先,什么时候不使用:

不要在代码中使用它,因为代码可以取任意一对起始&结束 迭代器,比如std::sort, std::find_if, std::copy等等 超泛型模板函数。如果你有一个标准,就不要用它 库容器(或Boost容器等) 适合您的代码。

任何一双开始&结束迭代器与连续存储的开始指针和结束指针相反。

作为一个很少接触迭代器内部的人,我在阅读答案时忘记了迭代器可以遍历链表,而简单的指针(和span)不能。