什么是“span”,在什么情况下应该使用它?

420

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

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


2
std::span是在2017年提出的。它适用于C++17或C++20。另请参见P0122R5,span:用于对象序列的边界安全视图。你真的想要以那种语言为目标吗?编译器追赶上来还需要几年时间。 - jww
9
@jww:使用 C++11,span非常适用……作为gsl::span而不是std::span。请参见下面的答案。 - einpoklum
同时也在cppreference.com上有文档记录:https://en.cppreference.com/w/cpp/container/span - Keith Thompson
6
2017年并非如此。 - einpoklum
8
所有编译器现在都支持 C++20 模式下的 std::span<>,而且 span 可以从许多第三方库中获取。你是正确的 - 它已经过去了几年:确切地说是 2 年。 - Contango
4个回答

495

它是什么?

span<T> 是:

  • 一个非常轻量级的内存中连续类型为 T 的值的抽象。
  • 基本上是一个带有许多便利方法的 struct { T * ptr; std::size_t length; }
  • 一种非拥有类型(即 "reference-type" 而不是 "value type"):它永远不会分配或释放任何内存,并且不会使智能指针保持有效。

它曾经被称为 array_view ,甚至更早的时候是 array_ref

我应该在什么时候使用它?

首先,应该使用 span 的情况:

  • 在代码中不要使用 span,如果只需要任何一对起始和结束迭代器(例如 std::sortstd::find_ifstd::copy 和其他来自 <algorithm> 的模板化函数)以及不接受任意范围的代码(有关这些,请参见 C++20 范围库的信息)。与一对迭代器或范围相比,span 具有更严格的要求:元素连续性和元素存在于内存中。
  • 如果您知道标准库容器(或 Boost 容器等)适合您的代码,请不要使用 span。spans 不打算取代现有容器。

现在让我们看看什么时候使用 span:

Use span<T> (respectively, span<const T>) instead of a free-standing T* (respectively const 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很棒!使用span...

  • means that you can work with that pointer+length / start+end pointer combination like you would with a fancy, pimped-out standard library container, e.g.:

    • 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)

    ... but with absolutely none of the overhead most container classes incur.

  • lets the compiler do more work for you sometimes. For example, this:

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

    becomes this:

    int buffer[BUFFER_SIZE];
    read_into(buffer);
    

    ... which will do what you would want it to do. See also Guideline P.5.

  • is the reasonable alternative to passing const vector<T>& to functions when you expect your data to be contiguous in memory. No more getting scolded by high-and-mighty C++ gurus!

  • facilitates static analysis, so the compiler might be able to help you catch silly bugs.

  • allows for debug-compilation instrumentation for runtime bounds-checking (i.e. span's methods will have some bounds-checking code within #ifndef NDEBUG ... #endif)

  • indicates that your code (that's using the span) doesn't own the pointed-to memory.

使用span有更多的动机,你可以在C++核心准则中找到,但你已经掌握了要领。

但它是否在标准库中?

编辑:是的,std::span已经在C++20版本中添加到了C++中!

为什么只有在C++20中才有?虽然这个想法并不新鲜——它的现在形式是与C++核心准则项目一起构思出来的,该项目直到2015年才开始形成。所以花费了一段时间。

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

它是Core Guidelines支持库(GSL)的一部分。实现:

  • Microsoft / Neil Macintosh的GSL包含一个独立的实现:gsl/span
  • GSL-Lite是整个GSL的单头文件实现(它并不大,不用担心),包括span<T>

GSL实现通常假设平台支持C++14 [12]。这些替代的单头文件实现不依赖于GSL设施:

请注意,这些不同的span实现在其提供的方法/支持函数方面存在一些差异;它们也可能与C++20标准库中采用的版本有所不同。


进一步阅读:您可以在C++17最终官方提案P0122R7中找到所有细节和设计考虑因素:span: 对象序列的边界安全视图,作者是Neal Macintosh和Stephan J. Lavavej。尽管有点长。此外,在C++20中,跨度比较语义发生了变化(遵循Tony van Eerd的这篇短文)。


6
更有意义的做法是标准化一个通用范围(支持迭代器+哨兵和迭代器+长度,甚至是迭代器+哨兵+长度),并使span成为一个简单的typedef。因为这样更加通用。 - Deduplicator
3
C++即将引入范围(Ranges)功能,但当前提议(由Eric Niebler提出)需要支持概念(Concepts),因此这个功能要等到C++20版本之后才能使用。 - einpoklum
8
数组不会立即衰变为指针。尝试执行std::cout << sizeof(buffer) << '\n',你会发现你得到的是100个整型的大小。 - einpoklum
7
@Jim:std::array是一个容器,它拥有其中的值。而span则是一种非拥有型容器。 - Caleth
4
@Jim:std::array 是一种完全不同的数据结构。它在编译时长度固定,并且是值类型而不是引用类型,如Caleth所解释的那样。 - einpoklum
显示剩余39条评论

51
一个 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++特性”和“愉悦感”。请注意:上面定义的结构体容器称为span,被我称为“轻量级的C风格数组包装器”,因为它指向一段连续的内存,如C风格数组,并用访问器方法和数组大小进行了包装。这就是我所说的“轻量级包装器”的含义:它是指针和长度变量加上函数的包装器。但与std::vector<>等其他C++标准容器不同的是,后者也可能只有固定的类大小,并包含指向其存储内存的指针,但span不拥有它所指向的内存,永远不会自动删除、调整大小或分配新内存。再次强调,像vector这样的容器拥有它所指向的内存,并将对其进行管理(分配、重新分配等),但span不拥有它所指向的内存,因此不会对其进行管理。

更进一步:

@einpoklum在他的回答中已经很好地介绍了span是什么,请参见此处。然而,即使阅读了他的回答,对于对span新手来说仍然容易产生一连串没有完全回答的思维问题,例如以下问题:

  1. span与C数组有何不同?为什么不直接使用一个C数组?它似乎只是具有已知大小的C数组...
  2. 等等,这听起来像是std::arrayspan与之有何不同?
  3. 哦,这让我想起来了,std::vector也像std::array一样吗?
  4. 我太困惑了。:( 什么是span

因此,这里提供了一些额外的清晰度:

他的回答的直接引用--带有我的补充说明和括号注释以及我的强调部分

它是什么?

span<T>是:

  • 类型为T的连续值序列在内存中的非常轻量级的抽象。
  • 基本上是一个{T * ptr; std::size_t length;}结构体与一堆方便的方法。(请注意,这与std::array<>明显不同,因为span通过指向类型T和长度(元素数)为类型T的指针启用了方便的访问器方法,类似于std::array,而std::array是实际容器,其中包含一个或多个类型为T。)
  • 非拥有类型(即"引用类型"而不是"值类型"):它从不分配也不释放任何东西,也不会保持智能指针的生命。
以前被称为array_view,更早是array_ref
以上加粗部分对理解至关重要,因此不要漏掉或误读!span不是C结构体数组,也不是类型为T的C数组和数组长度的结构体(这实际上就是std::array容器),也不是指向类型T的结构体指针的C数组加上长度,而是一个只包含单个指向类型T的指针和长度单一结构体,并且该长度是指该指向类型T的指针所指连续内存块中的元素数量(类型为T)。因此,使用span添加的唯一开销是用于存储指针和长度的变量,以及您使用的任何便利访问器函数,由span提供。
这与std::array<>不同,因为std::array<>实际上为整个连续块分配内存;它与std::vector<>不同,因为std::vector基本上只是一个std::array,它还可以进行动态增长(通常是加倍大小),每当它填满并尝试添加其他东西时。 std::array的大小是固定的;而span甚至不管理其指向的内存块的内存,它只是指向内存块,知道内存块的长度,知道内存中C数组中的数据类型,并提供方便的访问器函数以处理该连续内存中的元素。

它确实是C++标准的一部分:

std::span是C++20标准的一部分。您可以在此处阅读其文档:https://en.cppreference.com/w/cpp/container/span。要了解如何在今天的C++11或更高版本中使用Google的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

谷歌已将其内部 C++11 库开源,形成了他们的 "Abseil" 库。该库旨在提供 C++14 到 C++20 以及更高版本的功能,可在 C++11 及更高版本中使用,以便您可以今天就使用明天的功能。他们说:

与 C++ 标准的兼容性

Google 开发了许多抽象概念,这些概念与 C++14、C++17 以及更高版本中所包含的特性相匹配或非常接近。使用这些抽象概念的 Abseil 版本允许您立即访问这些功能,即使您的代码还没有准备好进入后 C++11 时代。

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

  1. 主站点: https://abseil.io/
  2. 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. [我在模板和跨度方面的另一个回答] 如何创建跨度的跨度

5
我不建议使用Abseil的全部功能来获取一个span类。 - einpoklum
1
从C++开发者的角度来看,我认为最大的优势不是“轻量级”,而是:“包装已经存在的C数组”,这样就不需要复制,现在你有了一个容器的包装器,它内部保存了其大小的信息,而C数组则不知道也不携带自己的大小信息。然而,作为一名嵌入式开发人员,我个人更喜欢使用原始的C数组,而不是span。 - Gabriel Staples
1
它不是C风格数组的包装器;std::array是C风格数组的包装器。std::span是对连续序列(例如C风格数组)的引用 - John McFarlane
@JohnMcFarlane,我知道你在说什么,但是你只是人为地限制了“包装器”一词的含义,使其意味着“不能仅包含指针或引用和长度”。这样做是不必要的狭隘。我做的第一件事是在我的答案顶部以代码和注释的形式提供了可能的 span 实现,并且与你的观点相符。然后,我把那个结构体容器称为 span,它是一个“轻量级的 C 风格数组封装器”,因为它指向一块连续的内存,并使用访问器方法和数组的大小进行封装。我称之为“轻量级的封装器”。 - Gabriel Staples
@JohnMcFarlane,同意。span是指针和长度变量(加上函数)的包装器。这就是我所说的“轻量级”。 - Gabriel Staples
显示剩余4条评论

5
Einpoklum提供的答案非常好,但我不得不深入评论部分才能理解一个具体细节,因此这篇文章旨在作为补充来澄清该细节。
首先,在以下情况下不要使用它:
像std::sort、std::find_if、std::copy等所有超通用模板函数一样,不要在可以接受任意起始和结束迭代器对的代码中使用它。如果你有一个标准库容器(或者Boost容器等),你知道它是适合你的代码的,请不要使用它。它并不打算取代它们。
任何起始和结束迭代器对与连续存储的起始和结束指针相反。 作为一个很少接触迭代器内部的人,当我阅读答案时,我忽略了迭代器可以遍历链表,而简单的指针(和span)却不能。

span 可以从连续的迭代器构造,但这些迭代器不一定是指针。例如,vector::iterator 是连续的,但不是指针。你引用的评论实际上归结为“使用合适的工具来完成任务”。如果你正在编写一个对任意序列进行操作的算法/实用程序,不要尝试将其擦除到 span 中,因为这会防止使用任何常规容器。然而,如果你想要一个表示非拥有连续数据的容器,那么 span 很可能是正确的选择。 - Human-Compiler
迭代器可以遍历链表、红黑树、哈希表(这些都在C++标准库中有实现),以及包括第三方异类容器在内的任何其他数据结构。 - Ben Voigt
我同意你们两个写的内容。这似乎是我在这里尝试总结/强调的事实的重新表述/重复。 - Tolar
1
这个回答启发了我对我的回答进行扩展。所以,给它点赞,但我希望现在当你阅读我的回答时,你所表达的观点已经足够明显了。 - einpoklum
我认为现在很明显了,但我不能再校对了,因为现在我知道了并且这是一个难以忘记的细节,所以我不知道之前的我是否会重复我的疏忽。我将把是否删除我的答案留给其他人来决定。 - Tolar

0
一个 span(或者更确切地说:拥有)指向不由该 span 拥有的数据的指针。实际上,您可以对 span 进行范围操作,从而影响其他人的数据。
这意味着您可以编写像原地快速排序这样的代码。
void qs( span<T> data ) {
   split(data);
   qs( span{ data+0,sizeoffirst } );
   qs( span{ data+sizeoffirst,sizeofsecond } );

通过递归或并行计算,可以使用Lapack风格的线性代数函数中的任意数量。

1
这更像是一条评论。 - einpoklum
编辑后,我的关于“何时使用它”的说明更清楚了吗? - Victor Eijkhout
这并没有回答问题。如果要对作者进行批评或请求澄清,请在他们的帖子下面留言。- 来自审查 - SwissCodeMen
我不知道你在告诉我什么关于批评或澄清。我既不在批评也不澄清。另外,你是C++专家吗?我只是提供了一个使用场景来回答问题。 - Victor Eijkhout
1
@SwissCodeMen 我没有看到关于对(非自有)子数组进行操作的观点在任何地方都有涵盖。线性代数的重要应用领域也没有被提及。对于应用科学家来说,这比它是轻量级的事实更加重要。所以关于“何时使用”只在我的回答中有涵盖:如果你进行线性代数运算,或者其他需要非自有子数组的操作(例如原地排序),那么你应该使用它。而这正是我的回答独特之处。 - Victor Eijkhout
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接