什么是“跨度”,何时应该使用?

What is a "span" and when should I use one?

提问人:einpoklum 提问时间:8/17/2017 最后编辑:einpoklum 更新时间:8/11/2023 访问量:163853

问:

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

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

C ++20 C cpp-core-guidelines std-span

评论

9赞 einpoklum 1/24/2018
@jww:span 在 C++11 中非常有用......as 而不是 .另请参阅下面的我的回答。gsl::spanstd::span
0赞 Keith Thompson 3/25/2020
还记录在 cppreference.com 上:en.cppreference.com/w/cpp/container/span
6赞 einpoklum 3/25/2020
@KeithThompson:不是在2017年,它不是......
8赞 Contango 6/20/2020
@jww 所有编译器都支持 std::span<>现在处于 C++20 模式。span 可从许多第三方库获得。你是对的——那是几年:准确地说是 2 年。

答:

495赞 einpoklum 8/17/2017 #1

这是什么?

A 是:span<T>

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

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

我应该什么时候使用它?

首先,何时使用 spans:

  • 不要在代码中使用可以只接受任何一对开始和结束迭代器(如 、 和其他模板化函数)的跨度,也不要在采用任意范围的代码中使用跨度(有关这些范围的信息,请参见 C++20 范围库)。span 比一对迭代器或范围有更严格的要求:元素的连续性和内存中元素的存在。std::sortstd::find_ifstd::copy<algorithm>
  • 如果您有一个标准库容器(或 Boost 容器等),则不要使用 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);

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

哦,跨度真棒!使用跨度...

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

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

    ...但是,大多数容器类绝对不会产生任何开销。

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

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

    变成这样:

    int buffer[BUFFER_SIZE];
    read_into(buffer);
    

    ...这将做你想让它做的事情。另见准则P.5

  • 是当您希望数据在内存中连续时传递给函数的合理替代方法。再也不用被高高在上的C++大师骂了!const vector<T>&

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

  • 允许对运行时边界检查进行调试编译检测(即 的方法将在...span#ifndef NDEBUG#endif)

  • 指示代码(使用范围)不拥有指向内存。

使用 s 的动机甚至更多,你可以在 C++ 核心指南中找到 - 但你抓住了漂移。span

但它在标准库中吗?

编辑:是的,std::span 是用 C++ 版本的语言添加到 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 中标准库中采用的版本略有不同。


延伸阅读:您可以在 C++17 之前的最终官方提案中找到所有细节和设计注意事项,P0122R7:Neal Macintosh 和 Stephan J. Lavavej 合著的 span: bounds-safe views for sequences of objects。不过有点长。此外,在 C++20 中,span 比较语义发生了变化(在 Tony van Eerd 的这篇简短论文之后)。

评论

6赞 Deduplicator 8/17/2017
标准化一般范围(支持迭代器+哨兵和迭代器+长度,甚至可能是迭代器+哨兵+长度)并使 span 成为简单的 typedef 会更有意义。因为,你知道,这更通用。
3赞 einpoklum 8/17/2017
@Deduplicator:Ranges 即将推出 C++,但目前的提案(由 Eric Niebler 提出)需要对 Concepts 的支持。所以不是在C++20之前。
8赞 einpoklum 8/23/2017
@HảiPhạmLê:数组不会立即衰减为指针。尝试这样做,你会看到你得到 100 sizeof(int) 的。std::cout << sizeof(buffer) << '\n'
7赞 Caleth 12/4/2018
@Jim是一个容器,它就拥有这些值。 是非所有权的std::arrayspan
4赞 einpoklum 12/4/2018
@Jim:是完全不同的野兽。正如 Caleth 所解释的那样,它的长度在编译时是固定的,它是一个值类型而不是引用类型。std::array
51赞 Gabriel Staples 4/15/2020 #2

A是这样的: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 并不拥有它所指向的内存,并且永远不会删除它、调整它的大小或自动分配新内存。同样,像向量这样的容器拥有它所指向的内存,并将管理(分配、重新分配等)它,但 span 不拥有它指向的内存,因此不会管理它。


更进一步:

@einpoklum很好地介绍了他在这里的回答中什么是A。然而,即使在阅读了他的答案之后,对于刚开始的人来说,仍然很容易有一系列没有完全回答的思想流问题,例如:span

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

因此,这里有一些额外的澄清:

直接引用他的回答--用粗体补充和括号注释用斜体强调

这是什么?

A 是:span<T>

  • 对内存中某处类型的连续值序列的非常轻量级的抽象。T
  • 基本上是一个带有一堆便利方法的结构体。(请注意,这与 std::array 明显不同<>因为 span 通过指向 T 类型的指针和 T 类型的长度(元素数)的指针,可以方便地访问访问器方法,类似于 std::array,而 std:array 是一个实际的容器,它包含一个或多个 T 类型的{ T * ptr; std::size_t length; }
  • 非拥有类型(即“引用类型”而不是“值类型”):它从不分配或释放任何内容,也不会使智能指针保持活动状态。

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

这些大胆的部分对一个人的理解至关重要,所以不要错过它们或误读它们!A 不是结构体的 C 数组,也不是类型加上数组长度的 C 数组的结构体(这本质上就是容器的样子),也不是类型指针加长度结构体的 C 数组,而是包含指向类型 T 的单个指针单个结构体, 和长度,即指向类型 T 的指针指向的连续内存块中的元素(类型为 T)的数量!这样,使用 a 添加的唯一开销是用于存储指针和长度的变量,以及用于提供的任何方便的访问器函数。spanTstd::arrayTspanspan

这与 a 不同,因为实际上为整个连续块分配内存,而 DIFFERENT 是因为 a 基本上只是一个,每次它填满并且您尝试向其添加其他内容时,它也会进行动态增长(通常大小翻倍)。A 的大小是固定的,span 甚至不管理它所指向的块的内存,它只是指向内存块,知道内存块有多长,知道内存中 C 数组中的数据类型,并提供方便的访问器函数来处理该连续内存中的元素std::array<>std::array<>std::vector<>std::vectorstd::arraystd::array

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

std::span是 C++ 标准的一部分,自 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
  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++ 标准的兼容性

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标头和模板类:https://github.com/abseil/abseil-cpp/blob/master/absl/types/span.h#L153absl::Span<T>(array, length)

其他参考资料:

  1. 在 C++ 中使用模板变量的结构
  2. 维基百科:C++类
  3. C++ 类/结构成员的默认可见性

相关:

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

评论

5赞 einpoklum 8/28/2020
我真的不建议使用所有的绳降来获得跨度课程。
0赞 yushang 7/1/2021
明白了。最大的优点是重量轻。
1赞 Gabriel Staples 8/9/2021
@yushang,从C++开发人员的角度来看,我认为最大的优势不是“轻量级”,而是:“包裹已经存在的 C 数组”,这样就不需要复制了,你现在有一个容器周围的包装器,它的大小保持在里面,不像 C 数组,它不知道也不携带有关自己大小的信息。然而,作为一名嵌入式开发人员,我比自己在 C++ 方面有更多的经验,我通常更喜欢在一段时间内只使用原始 C 数组。
0赞 Chef Gladiator 8/13/2021
void array_operator ( const size_t count_, char arr [ static count_ ] );这是标准 C。在那里有一个完整的数组信息。Plus 必须具有 min 元素。上下文是这个讨论,而不是“C 更好”的咆哮。arrcount_
1赞 John McFarlane 11/2/2021
它不是 C 样式数组的包装器; 是 C 样式数组的包装器。 是对连续序列(如 C 样式数组)的引用std::arraystd::span
5赞 Tolar 6/30/2022 #3

Einpoklum 提供的答案很棒,但我必须深入研究评论部分才能了解一个具体细节,因此这是为了澄清该细节的扩展。

首先,何时不使用它:

不要在可能只需要任何一对开始和结束的代码中使用它 迭代器,如 std::sort、std::find_if、std::copy 等等 超级通用的模板化函数。如果您有标准,请不要使用它 库容器(或 Boost 容器等),您知道它是 适合您的代码。它无意取代其中任何一个。

任何一对开始和结束迭代器,而不是连续存储的开始和结束指针。

作为一个很少接触迭代器内部的人,在我读到迭代器可以迭代链表的答案时,我逃脱了,而简单的指针(和跨度)则不能。

评论

0赞 Human-Compiler 7/6/2022
spans 可由连续的迭代器构造,这些迭代器不一定是指针。 是连续的,但不是指针 -- 例如。您引用的评论有效地归结为“为工作使用正确的工具”;如果您正在任意序列上编写算法/实用程序,请不要尝试将其擦除到 a 中,因为这会阻止使用任何正常的容器。但是,如果您希望拥有一个表示非拥有连续数据的容器,则可能是正确的选择。vector::iteratorspanspan
0赞 Ben Voigt 7/14/2022
迭代器可以迭代......一个链接列表,一个红黑树,一个哈希图(所有这些都在C++标准库中都有实现),以及其他任何东西,包括第三方外来容器。
0赞 Tolar 7/21/2022
我同意你们俩写的。这似乎是对我试图在这里总结/强调的事实的改写/重复。
1赞 einpoklum 9/11/2022
这个答案启发了我稍微扩展我的。所以,+1,但我希望在阅读我的答案时,你现在的观点已经足够明显了。
0赞 Tolar 9/27/2022
我认为现在很明显,但我不能再校对了,因为现在我知道了,这是一个很难忘记的细节,所以我不知道早些时候我是否会重复我的疏忽。我会把它留给其他人来决定是否应该删除我的答案。
0赞 Victor Eijkhout 8/11/2023 #4

跨度是(或者更确切地说:具有)指向不属于该跨度的数据的指针。实际上,您可以对影响其他人数据的跨度执行远程操作。

这意味着您可以编写类似就地快速排序的内容:

void qs( span<T> data ) {
   split(data);
   qs( span{ data+0,sizeoffirst } );
   qs( span{ data+sizeoffirst,sizeofsecond } );

或通过递归或并行的任意数量的拉帕克式线性代数函数。

评论

1赞 einpoklum 8/11/2023
这更像是一种评论。
0赞 Victor Eijkhout 8/11/2023
编辑后,我对“何时应该使用它”的理解是否更清晰?
0赞 SwissCodeMen 8/15/2023
这并不能提供问题的答案。要批评或要求作者澄清,请在其帖子下方发表评论。 - 来自评论
0赞 Victor Eijkhout 8/15/2023
@SwissCodeMen我不知道你在告诉我关于批评或澄清的什么。我也不做。另外:您是 C++ 专家吗?我给出了一个可以使用的场景,它回答了这个问题。
1赞 Victor Eijkhout 8/15/2023
@SwissCodeMen 我看不出我对任何地方覆盖的(非自有)子阵列进行操作的意义。线性代数的重要应用领域在任何地方都没有提到。对于应用科学家来说,这比它是轻量级的东西要重要得多。因此,“我什么时候应该使用一个”只在我的回答中涉及:如果你做线性代数,或者其他操作(比如就地排序),你想要一个非拥有的子数组。这是我的答案所独有的。