当前位置: 首页 > article >正文

std::span

 C++20中引入了std::span<>类,用来引用元素序列而不需要分配内容。其中std::span<>类模板就是其中之一。要求元素存储在连续的内容中,std::span<>类为子序列提供了最佳性能。

      std::span<>类型引用的对象元素必须是一个接一个的存储在连续的存储中,并且使用时具有只读或者写的访问。

     实际上,std::span就像指向一系列元素的原始指针并结合一个元素数量的大小数字。允许我们处理像数组这样的元素。然而,由于其引用语义(尤其是关于const正确性),特殊规则被使用。

     使用std::span及廉价又快(应该始终按值传递它).然而,它也可能这很危险,因为与原始指针一样,程序员需要确保在使用span时所引用的元素序列仍然有效。此外,span支持写访问可能会导致破坏const正确性的情况(或者至少不按您预期的方式工作)。

9.1 使用span

当你声明一个 span 时,你可以选择指定一个固定数量的元素,或者保留元素数量的开放性,使得 span 所引用的元素数量可以改变。

9.1.1 固定和动态长度(Extent)

当你声明span的时候,可以选择指定一个固定数量的元素或者也可以保留该数量,直到明确span所指的元素的数量。

具有指定固定元素数量的 span 被称为具有固定长度(extent)的 span。它可以通过指定元素类型和大小作为模板参数来声明,或者通过使用数组(原始数组或 std::array<>)的迭代器和大小进行初始化来声明。

int a5[5] = {1, 2, 3, 4, 5};

std::array arr5{1, 2, 3, 4, 5};

std::vector vec{1, 2, 3, 4, 5, 6, 7, 8};

std::span sp1 = a5; // span with fixed extent of 5 elements

std::span sp2{arr5}; // span with fixed extent of 5 elements

std::span<int, 5> sp3 = arr5; // span with fixed extent of 5 elements

std::span<int, 3> sp4{vec}; // span with fixed extent of 3 elements

std::span<int, 4> sp5{vec.data(), 4}; // span with fixed extent of 4 elements

std::span sp6 = sp1; // span with fixed extent of 5 elements

对于这样的 span,成员函数 size() 总是返回作为类型的一部分指定的大小。除非长度(extent)为 0,否则不能调用默认构造函数。

当 span 的元素数量在其生命周期内不稳定时,称为具有动态长度(extent)的 span。元素数量取决于 span 所引用的元素序列,并且可能由于分配新的基础范围(没有其他方式可以改变元素数量)而发生变化。例如:

int a5[5] = {1, 2, 3, 4, 5};

std::array arr5{1, 2, 3, 4, 5};

std::vector vec{1, 2, 3, 4, 5, 6, 7, 8};

std::span<int> sp1; // span with dynamic extent (initially 0 elements)

std::span sp2{a5, 3}; // span with dynamic extent (initially 3 elements)

std::span<int> sp3{arr5}; // span with dynamic extent (initially 5 elements)

std::span sp4{vec}; // span with dynamic extent (initially 8 elements)

std::span sp5{arr5.data(), 3}; // span with dynamic extent (initially 3 elements)

std::span sp6{a5+1, 3}; // span with dynamic extent (initially 3 elements)

请注意,由程序员来确保 span 引用的范围是有效的,并且具有足够的元素。

对于这两种情况,让我们看一些完整的示例。

9.1.2 使用动态范围span的示例

这里介绍一个动态长度(extent)的第一个例子:

#include <iostream>
#include <string>
#include <vector>
#include <ranges>
#include <algorithm>
#include <span>

template<typename T, std::size_t Sz>
void printSpan(std::span<T, Sz> sp)
{
  for (const auto& elem : sp) {
    std::cout << '"' << elem << "\" ";
  }
  std::cout << '\n';
}

int main()
{
  std::vector<std::string> vec{"New York", "Tokyo", "Rio", "Berlin", "Sydney"};

  // define view to first 3 elements:
  std::span<const std::string> sp{vec.data(), 3};
  std::cout << "first 3:                   ";
  printSpan(sp);

  // sort elements in the referenced vector:
  std::ranges::sort(vec);
  std::cout << "first 3 after sort():      ";
  printSpan(sp);

  // insert a new element:
  // - must reassign the internal array of the vector if it reallocated new memory
  auto oldCapa = vec.capacity();
  vec.push_back("Cairo");
  if (oldCapa != vec.capacity()) {
    sp = std::span{vec.data(), 3};
  }
  std::cout << "first 3 after push_back(): ";
  printSpan(sp);

  // let span refer to the vector as a whole:
  sp = vec;
  std::cout << "all:    ";
  printSpan(sp);  

  // let span refer to the last five elements:
  sp = std::span{vec.end()-5, vec.end()};
  std::cout << "last 5: ";
  printSpan(sp);

  // let span refer to the last four elements:
  sp = std::span{vec}.last(4);
  std::cout << "last 4: ";
  printSpan(sp);
}

程序输出:

first 3: "New York" "Tokyo" "Rio"

first 3 after sort(): "Berlin" "New York" "Rio"

first 3 after push_back(): "Berlin" "New York" "Rio"

all: "Berlin" "New York" "Rio" "Sydney" "Tokyo" "Cairo"

last 5: "New York" "Rio" "Sydney" "Tokyo" "Cairo"

last 4: "Rio" "Sydney" "Tokyo" "Cairo"

介绍下这部分代码:

定义一个span

在 main() 函数中,我们首先使用vector的前三个元素来初始化一个包含三个const string的 span:

std::vector<std::string> vec{"New York", "Rio", "Tokyo", "Berlin", "Sydney"};

std::span<const std::string, 3> sp3{vec.begin(), 3};

在初始化时,我们传递了序列的起始位置和元素数量。在这种情况下,我们引用了 vec 的前三个元素。

关于这个声明有很多需要注意的事项:

  1. 由程序员负责确保元素的数量与 span 的长度匹配,并且元素是有效的。如果向量没有足够的元素,行为是未定义的

std::span<const std::string, 3> sp3{vec.begin(), 10}; // undefined behavior

  1.  永远不要用(返回的)临时值初始化span,因为span引用的对象在赋值后就被销毁了:

std::span<int, 3> sp3a = std::array{1, 2, 3}; // fatal runtime ERROR

std::span<int, 3> sp3b = returnArray(); // fatal runtime ERROR

  1. 需要指出的是通过指定span元素为const string类型的,我们无法通过span修改他们的值。注意,声明span为const span的话并没有提供只读访问给引用元素:

std::span<const std::string, 3> sp3{vec.begin(), 3}; // elements cannot be modified by sp3

const std::span<std::string, 3> sp3{vec.begin(), 3}; // elements can be modified

  1. 在 span 的元素类型与引用元素的类型不同时,看起来好像可以使用任何转换为基础元素类型的类型。然而,这是不正确的。你只能添加诸如 const 之类的限定符:

std::vector<int> vec{ ... };

std::span<long, 3> sp3{vec.begin(), 3}; // ERROR

传递和输出span

下一步,输出span,传递span到一个泛型print函数:

printSpan(sp3);

该打印函数可以处理任何的span(元素的输出运算符必须得定义了)

template<typename T, std::size_t Sz>
void printSpan(std::span<T, Sz> sp)

{

    for (const auto& elem : sp) 
    {
        std::cout << '"' << elem << "\" ";
    }

    std::cout << '\n';

}

你可能会感到惊讶,即使函数模板 printSpan<>() 具有用于 span 大小的非类型模板参数,它也可以被调用。这是因为 std::span<T> 是一个具有伪大小 std::dynamic_extent 的 span 的快捷方式。

std::span<int> sp; // equivalent to std::span<int, std::dynamic_extent>

实际上,类模板std::span被声明为如下:

namespace std {

template<typename ElementType, size_t Extent = dynamic_extent>

class span {

...

};

}

这使得程序员可以提供适用于具有固定长度(extent)和具有动态长度(extent)的 span 的通用代码,例如 printSpan<>()。当使用具有固定长度(extent)的 span 调用 printSpan<>() 时,长度(extent)会作为模板参数传递:

std::span<int, 5> sp{ ... };

printSpan(sp); // calls printSpan<int, 5>(sp)

正如你所看到的,span 是按值传递的。这是推荐的传递 span 的方式,因为它们在内部只是一个指针和一个大小,所以复制起来很廉价。

在 printSpan 函数内部,我们使用range-based循环来迭代 span 的元素。这是可能的,因为 span 提供了以 begin() 和 end() 支持的迭代器。

然而,需要注意的是:无论我们是按值传递还是按 const 引用传递 span,在函数内部我们仍然可以修改元素,只要它们没有被声明为 const(std::span<const std::string, 3> sp3{vec.begin(), 3};)这就是为什么通常将 span 的元素声明为 const 是有意义的。

#include <iostream>
#include <string>
#include <vector>
#include <span>

template<typename T, std::size_t Sz>
//void printSpan(const std::span<T, Sz>& sp)//跟下面这行代码一样都可以修改sp的值
void printSpan(std::span<T, Sz> sp)
{
	for (auto& elem : sp) 
	{
	    std::cout << '"' << elem << "\" ";
        elem += "X";
    }
    std::cout << '\n';
}

int main()
{
	std::vector<std::string> vec{"New York", "Tokyo", "Rio", "Berlin", "Sydney"};
	std::span<std::string, 3> sp3{vec.begin(), 3};
	std::cout << "first 3: ";
	printSpan(sp3);
	
	printSpan(sp3);
}

处理引用语义

接下来,我们对span所引用的元素进行排序(我们在这里使用了新的std::ranges::sort,它将整个容器作为参数):

std::ranges::sort(vec);

std::vector<std::string> vec{"New York", "Rio", ... }; // elements not const


std::span<std::string, 3> span3{vec.begin(), 3};   //

std::ranges::sort(span3); // sort first 3 elements only


std::span<const std::string, 3> span4{vec.begin(), 3};

std::ranges::sort(span4); // error: no match for call to ‘(const std::ranges::__sort_fn) (std::span, 3>&)

由于span具有引用语义,排序同样也影响了span:进行了同样的排序。如果我们没有给span使用const给元素类型(std::span<const Type, Size>),也可以使用span调用sort排序。

#include <iostream>
#include <string>
#include <vector>
#include <span>
#include <ranges>
#include <algorithm>
template<typename T, std::size_t Sz>
void printSpan(const std::span<T, Sz>& sp)
{
	for (auto& elem : sp) 
	{
	    std::cout << '"' << elem << "\" ";
        //elem += "X";
    }
    std::cout << '\n';
}

int main()
{
	std::vector<std::string> vec{"New York", "Tokyo", "Rio", "Berlin", "Sydney"};
	std::span<std::string, 3> sp3{vec.begin(), 3};   //

	printSpan(sp3);
	std::ranges::sort(sp3); // sort first 3 elements only
	printSpan(sp3);
    for(const auto& s : vec)
    {
        std::cout << '"' << s << "\" ";
    }
}

    引用语义意味着在使用 span 时必须小心,这在示例中展示了下面的语句。在这里,我们向包含 span 所引用的元素的向量中插入一个新元素。由于 span 的引用语义,这是我们必须非常小心的事情,因为如果vector重新分配内存,它将使所有迭代器和指向其元素的指针失效。因此,重新分配还会使引用vector元素的 span 失效。span 引用的元素已经不存在了。

因此,我们在插入之前和之后双重检查容量(已分配内存的最大元素数量)。如果容量发生变化,我们将重新初始化 span,引用新内存中的前三个元素:

auto oldCapa = vec.capacity();

vec.push_back("Cairo");

if (oldCapa != vec.capacity())

 {

     sp = std::span{vec.data(), 3};

}

我们之所以可以进行这种重新初始化,是因为 span 本身不是 const。

下一步,我们插入了一个新的元素到span引用的vector中。由于span引用语义,这里就要格外小心了。因为如果vector分配新的内存将使所有迭代器和指向其元素的指针无效。因此,重新分配也会使引用给vectorspan失效,span指向的是已经不存在的元素。

    因此,我们再次检查vector capacity(内存的最大元素数已分配)。如果它改变了,我们重新初始化span以引用前三个新内存中的元素:

auto oldCapa = vec.capacity();

vec.push_back("Cairo");

if (oldCapa != vec.capacity())

{

    span3 = std::span<std::string, 3>{vec.begin(), 3};

}

我们只能执行此重新初始化,因为span本身不是常量(不是const std::span<std::string, 3>)。

赋值容器给span

我们将整个vector分配给span,并将其打印出来:

std::span<const std::string> sp{vec.begin(), 3};

printSpan(sp); // 打印vector的前3个元素

sp = vec;

printSpan(sp); // 打印vector的所有元素

在这里,你可以看到对具有动态长度(extent)的 span 的赋值操作可以改变元素的数量。

Span 可以接受任何类型的容器或range,只要容器以连续的内存存放元素,并且提供通过成员函数 data() 访问元素。

然而,由于模板类型推导的限制,你不能将这样的容器传递给期望一个 span 的函数。你必须明确指定要将向量转换为 span:

printSpan(vec); // ERROR: template type deduction doesn’t work here

printSpan(std::span{vec}); // OK

分配不同的子序列

通常情况下,span 的赋值运算符允许我们分配另一个元素序列。示例中使用了这个功能来后续引用向量中的最后三个元素:

std::span<const std::string> sp{vec.data(), 3};

...

// assign view to last five elements:

sp = std::span{vec.end()-5, vec.end()};

在这里,你可以看到我们可以使用两个迭代器来指定引用的序列,这两个迭代器定义了序列的起始和结束,作为一个半开区间(包含起始值,不包含结束值)。要求是起始和结束都满足 std::sized_sentinel_for 概念,以便构造函数可以计算差异。

然而,正如下面的语句所示,也可以使用 span 的成员函数来分配最后的 n 个元素:

std::vector<std::string> vec{"New York", "Tokyo", "Rio", "Berlin", "Sydney"};

std::span<const std::string> sp{vec.data(), 3};

...

// assign view to last four elements:

sp = std::span{vec}.last(4);

Span 是唯一提供一种方式来获取range中间或末尾序列的视图。只要元素类型匹配,你可以传递任何其他类型的元素序列。例如:

std::vector<std::string> vec{"New York", "Tokyo", "Rio", "Berlin", "Sydney"};

std::span<const std::string> sp{vec.begin(), 3};

...

std::array<std::string, 3> arr{"Tick", "Trick", "Track"};

sp = arr; // OK

但是要注意,span 不支持元素类型的隐式类型转换(除了添加 const)。例如,以下代码无法编译通过:

std::span<const std::string> sp{vec.begin(), 3};

...

std::array arr{"Tick", "Trick", "Track"}; // deduces std::array<const char*, 3>

sp = arr; // ERROR: different element types

9.1.3 使用非const元素Span的示例

在初始化 span 时,我们可以使用类模板参数推导,以便推断元素的类型(和长度)

std::span sp{vec.begin(), 3}; // deduces: std::span<std::string>

然后,span 声明元素的类型为底层range的元素类型,这意味着你甚至可以修改底层range的值,前提是底层range没有将其元素声明为 const。

这个特性可以用来允许 span 在一条语句中修改range的元素。例如,你可以对元素的子集进行排序,如下面的示例所示:

#include <iostream>
#include <string>
#include <vector>
#include <ranges>
#include <algorithm>
#include <span>

void print(std::ranges::input_range auto&& coll)
{
  for (const auto& elem : coll) {
    std::cout << '"' << elem << "\" ";
  }
  std::cout << '\n';
}

int main()
{
  std::vector<std::string> vec{"New York", "Tokyo", "Rio", "Berlin", "Sydney"};
  print(vec);

  // sort the three elements in the middle:
  std::ranges::sort(std::span{vec}.subspan(1, 3));
  print(vec);

  // print last three elements:
  print(std::span{vec}.last(3));
}

在这里,我们创建临时的 spans 来对vector类型 vec 中的元素子集进行排序,并打印向量的最后三个元素。

程序的输出如下:

"New York" "Tokyo" "Rio" "Berlin" "Sydney"

"New York" "Berlin" "Rio" "Tokyo" "Sydney"

"Rio" "Tokyo" "Sydney"

Spans是视图。为了处理range的前n个元素,你还可以使用range工厂std::views::counted(),如果对一个具有连续内存元素的range的迭代器调用它,它会创建一个具有动态长度(extent)的span。

std::vector<int> vec{1, 2, 3, 4, 5, 6, 7, 8, 9};

auto v = std::views::counted(vec.begin()+1, 3); // span with 2nd to 4th elem of vec

9.1.4 使用固定长度(extent)Span的示例

作为一个具有固定长度(extent)的 span 的第一个示例,让我们将前面的示例进行转换,但是声明一个具有固定长度(extent)的 span。

#include <iostream>
#include <string>
#include <vector>
#include <ranges>
#include <algorithm>
#include <span>

template<typename T, std::size_t Sz>
void printSpan(std::span<T, Sz> sp)
{
  for (const auto& elem : sp) {
    std::cout << '"' << elem << "\" ";
  }
  std::cout << '\n';
}

int main()
{
  std::vector<std::string> vec{"New York", "Tokyo", "Rio", "Berlin", "Sydney"};

  // define view to first 3 elements:
  std::span<const std::string, 3> sp3{vec.data(), 3};
  std::cout << "first 3:              ";
  printSpan(sp3);

  // sort referenced elements:
  std::ranges::sort(vec);
  std::cout << "first 3 after sort(): ";
  printSpan(sp3);

  // insert a new element:
  // - must reassign the internal array of the vector if it reallocated new memory
  auto oldCapa = vec.capacity();
  vec.push_back("Cairo");
  if (oldCapa != vec.capacity()) {
    sp3 = std::span<std::string, 3>{vec.data(), 3};
  }
  std::cout << "first 3: ";
  printSpan(sp3);

  // let span refer to the last three elements:
  sp3 = std::span<const std::string, 3>{vec.end()-3, vec.end()};
  std::cout << "last 3:  ";
  printSpan(sp3);

  // let span refer to the last three elements:
  sp3 = std::span{vec}.last<3>();
  std::cout << "last 3:  ";
  printSpan(sp3);
}

程序输出如下:

first 3: "New York" "Tokyo" "Rio"

first 3 after sort(): "Berlin" "New York" "Rio"

first 3: "Berlin" "New York" "Rio"

last 3: "Sydney" "Tokyo" "Cairo"

last 3: "Sydney" "Tokyo" "Cairo"

让我们逐步介绍程序示例中的显著部分。

定义span

这次,我们首先用固定长度(extent)初始化了一个包含三个常量字符串的 span:

std::vector<std::string> vec{"New York", "Rio", "Tokyo", "Berlin", "Sydney"};

std::span<const std::string, 3> sp3{vec.data(), 3};

对于固定范围,我们同时指定了元素类型和大小。

同样,程序员需要确保元素的数量与 span 的范围匹配。如果作为第二个参数传递的计数与范围的大小不匹配,行为是未定义的

std::span<const std::string, 3> sp3{vec.begin(), 4}; // 未定义的行为

分配不同的子序列

对于具有固定长度(extent)的 span,你只能分配具有相同数量元素的新底层range。因此,这次我们只分配具有三个元素的 span:

std::span<const std::string, 3> sp3{vec.data(), 3};

...

sp3 = std::span<const std::string, 3>{vec.end()-3, vec.end()};//分配给sp3的是vec的最后3个元素,与sp3的固定长度(extent)是匹配的

请注意,以下代码将无法编译通过:

std::span<const std::string, 3> sp3{vec.data(), 3};

...

sp3 = std::span{vec}.last(3); // ERROR

原因是赋值运算符右侧的表达式创建了一个具有动态长度(extent)的 span。

然而,通过使用 last() 并指定模板参数的方式,我们可以获得一个具有相应固定范围的 span

std::span<const std::string, 3> sp3{vec.data(), 3};

...

sp3 = std::span{vec}.last<3>(); // OK

我们仍然可以使用类模板参数推导来分配数组的元素,甚至直接进行分配:

std::span<const std::string, 3> sp3{vec.data(), 3};

...

std::array<std::string, 3> arr{"Tic", "Tac", "Toe"};

sp3 = std::span{arr}; // OK

sp3 = arr; // OK

9.1.5 span固定长度和动态长度的对比(Spans with fixed vs. dynamic Extent)

固定长度和动态长度的span都有各自的优点。

指定固定长度可以更好地检测违规情况(在运行时甚至在编译时)。例如,您不能将具有错误数量元素的std::array<>赋值给具有固定范围的span:

std::vector vec{1, 2, 3};

std::array arr{1, 2, 3, 4, 5, 6};

std::span<int, 3> sp3{vec};

std::span sp{vec};

sp3 = arr; // compile-time ERROR

sp = arr; // OK

具有固定长度(extent)的 span 还需要更少的内存,因为它们不需要为实际大小保留成员(大小是类型的一部分)。

使用动态长度(extent)的 span 提供了更大的灵活性:

std::span<int> sp; // OK

...

std::vector vec{1, 2, 3, 4, 5};

sp = vec; // OK (span has 5 elements)

sp = {vec.data()+1, 3}; // OK (span has 3 elements)

9.2 Spans视为有害

Spans指的是对外部值的序列引用。因此,它们具有具有引用语义的类型通常所具有的问题。程序员需要确保 span 引用的序列是有效的。

错误很容易发生。例如,如果函数 getData() 通过值返回一组 int(例如,作为 vector、std::array 或原始数组),那么以下语句将导致致命的运行时错误:

std::span<int, 3> first3{getData()}; // ERROR: reference to temporary object

std::span sp{getData().begin(), 3}; // ERROR: reference to temporary object

sp = getData(); // ERROR: reference to temporary object

这可能看起来有点微妙,比如使用范围-based for 循环:

// for the last 3 returned elements:

for (auto s : std::span{arrayOfConst()}.last(3)) // fatal runtime ERROR

这段代码会导致未定义行为,因为由于范围-based for 循环中的一个 bug,对临时对象的引用在值已经被销毁时使用(参见http://wg21.link/p2012 获取详细信息)。

译器可以通过对标准类型进行特殊的“生命周期检查”来检测这些问题,目前主要的编译器正在实施这一功能。然而,这只能检测简单的生命周期依赖关系,比如 span 和其初始化的对象之间的依赖关系。

此外,您还必须确保所引用的元素序列保持有效。如果程序的其他部分在 span 仍在使用时结束了引用序列的生命周期,这可能会成为一个问题。如果我们引用对象(例如 vector),那么这个有效性问题甚至可能发生在 vector 仍然存在的情况下。考虑以下示例:

std::vector<int> vec{1, 2, 3, 4, 5, 6, 7, 8};

std::span sp{vec}; // view to the memory of vec

...

vec.push_back(9); // might reallocate memory

std::cout << sp[0]; // fatal runtime ERROR (referenced array no longer valid)

作为一种解决方法,您需要使用原始向量重新初始化 span。

总的来说,使用 spans 和使用原始指针及其他视图类型一样具有风险。请谨慎使用它们。

9.3 span的设计方面

设计一个引用序列元素的类型并不容易。需要考虑和决定许多方面和权衡:

  • 性能与安全性
  • const正确性
  • 可能的隐式和显式类型转换
  • 对支持的类型的要求
  • 支持的API

首先,让我明确一些事实:

  • span不是一个容器。它可能具有一些容器的属性(例如,通过begin()和end()迭代元素的能力),但由于其引用语义,已经存在一些问题:

  • 如果span是const,元素是否也应该是const?
  • 赋值操作的含义是什么:分配一个新序列还是为引用的元素分配新值?
  • 是否应该提供swap(),它的作用是什么?

  • span不是一个带有大小的指针。提供*和->运算符是没有意义的。

std::span类型是对一个特定元素序列的引用。正确使用这种类型很重要。Barry Revzin撰写了一篇非常有帮助的博文,强烈推荐你阅读: span: the best span | Barry's C++ Blog。

值得注意的是,C++20还提供了其他处理(子)序列引用的方法,例如子范围(subrange)。它们也适用于不存储在连续内存中的序列。通过使用range工厂std::views::counted(),你可以让编译器决定哪种类型最适合由起始位置和大小定义的范围。

9.3.1 span的声明周期依赖

由于引用语义的缘故,您只能在底层值序列存在时迭代span。然而,迭代器并不与它们创建的span的生命周期绑定。

span的迭代器并不引用它们创建的span。相反,它们直接引用底层的范围。因此,span是一个借用的范围。这意味着即使span不再存在(当然,元素序列仍然存在),您仍然可以使用迭代器。然而,请注意,当底层范围不再存在时,迭代器仍然可能无效(dangle)。

9.3.2 span的性能

Spans的设计目标是实现最佳性能。为此,它们在内部只使用原始指针来引用元素序列。然而,原始指针期望元素在内存中顺序存储在一个块中(否则,原始指针无法使用++来迭代元素)。因此,spans要求元素在连续的内存中。

基于这个要求,spans可以提供所有视图类型中最佳的性能。Spans不需要任何分配,也不带有任何间接性。使用span的唯一开销是构造它所需的开销。

Spans在编译时通过concepts检查来验证所引用序列的元素是否在连续的内存中。当初始化序列或分配新序列时,迭代器必须满足std::contiguous_iterator概念,而容器/范围必须满足std::ranges::contiguous_range和std::ranges::sized_range概念。

由于spans在内部只是一个指针和一个大小,所以复制它们非常廉价。因此,应始终优先通过值传递spans,而不是通过const引用传递。

类型擦除

spans使用原始指针来进行元素访问,这意味着span类型擦除了这些元素所在的容器的信息。对于vector的元素和array的元素,span具有相同的类型(前提是它们具有相同的大小):

std::array arr{1, 2, 3, 4, 5};

std::vector vec{1, 2, 3, 4, 5};

std::span<int> vecSpanDyn{vec};

std::span<int> arrSpanDyn{arr};

std::same_as<decltype(arrSpanDyn), decltype(vecSpanDyn)> // true

然而,请注意,对于span的类模板参数推导,从array中推导出固定的大小,从vector中推导出动态大小。这意味着:

std::array arr{1, 2, 3, 4, 5}; // 推导为std::span<int, 5>

std::vector vec{1, 2, 3, 4, 5}; // 推导为std::span<int>

Std::span<int, 5> arrSpan{arr};

std::span<int, 5> vecSpan5{vec};

std::same_as<decltype(arrSpan), decltype(vecSpan)> // false

std::same_as<decltype(arrSpan), decltype(vecSpan5)> // true

Span vs subranges

Spans和Subranges是C++20引入的两个概念。与subranges相比,spans需要元素的连续存储,这是它们的主要区别。Subranges仍然使用迭代器,因此可以引用所有类型的容器和范围。然而,这可能会导致更多的开销。此外,spans不需要所引用类型的迭代器支持。您可以传递任何提供data()成员以访问元素序列的类型。

8.3.2 spanconst

Span是具有引用语义的视图。从这个意义上讲,它们的行为类似于指针:当一个span是const时,并不意味着span引用的元素是const的。

这意味着您可以对const span的元素进行写访问(前提是元素不是const的):

std::array a1{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

std::array a2{0, 8, 15};

const std::span<int> sp1{a1}; // span/view是const的

std::span<const int> sp2{a1}; // 元素是const的

sp1[0] = 42; // OK

sp2[0] = 42; // ERROR

sp1 = a2; // ERROR

sp2 = a2; // OK

请注意,只要std::span<>的元素没有被声明为const,一些操作将提供对元素的写访问权限,即使对于const span,您可能不希望如此(遵循普通容器的规则):

• operator[], first(), last()

• data()

• begin(), end(), rbegin(), rend()

• std::cbegin(), std::cend(), std::crbegin(), std::crend()

• std::ranges::cbegin(), std::ranges::cend(), std::ranges::crbegin(), std::ranges::crend()

所有设计用于确保元素为const的c*函数在std::span中都失效了。

例如:

emplate<typename T>

void modifyElemsOfConstCollection (const T& coll)

{

    coll[0] = {}; // 对于spans来说是OK的,对于普通容器来说是ERROR的

    auto ptr = coll.data();

    *ptr = {}; // 对于spans来说是OK的,对于普通容器来说是ERROR的

    for (auto pos = std::cbegin(coll); pos != std::cend(coll); ++pos)

    {

        *pos = {}; // 对于spans来说是OK的,对于普通容器来说是ERROR的

    }

}

std::array a1{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

modifyElemsOfConstCollection(a1); // ERROR:元素是const的

modifyElemsOfConstCollection(std::span{a1}); // OOPS:OK,修改了数组的元素

问题并不在于std::span被破坏了;问题在于std::cbegin()等函数目前对具有引用语义(如视图)的集合而言是错误的。

为了确保一个函数只接受不可通过这种方式修改元素的序列,您可以要求const容器的begin()返回指向const元素的迭代器:

template<typename T>

void ensureReadOnlyElemAccess (const T& coll)

requires std::is_const_v<std::remove_reference_t<decltype(*coll.begin())>>

{

    ...

}

至少在标准化C++20之后,std::cbegin()甚至提供写访问权限是正在讨论的内容。提供cbegin()和cend()的整个目的是确保在迭代过程中不能修改元素。最初,spans确实提供了const_iterator、cbegin()和cend()成员以确保您不能修改元素。然而,事实证明std::cbegin()仍然对可变元素进行迭代(std::ranges::cbegin()也存在同样的问题)。但是修复std::cbegin()(和std::ranges::cbegin())时,span中const迭代器的成员为已删除(请参阅http://wg21.link/lwg3320),,这使问题变得更糟,因为现在没有简单的方法来进行只读迭代span。解决问题的正确方法是修复std::cbegin()的定义方式。我们正在为C++23进行修复(参见http://wg21.link/p2276和http://wg21.link/p2278 )

8.3.3 在通用代码中使用 Spans 作为参数

根据当前的写法,可以使用以下声明为所有的 span 实现一个通用函数:

template<typename T, std::size_t Sz>

void printSpan(std::span<T, Sz> sp);

这甚至适用于具有动态大小的 span,因为它们只需使用特殊值 std::dynamic_extent 作为大小。

因此,在实现中,你可以按照以下方式处理固定大小和动态大小之间的差异:

#ifndef SPANPRINT_HPP

#define SPANPRINT_HPP

#include <iostream>

#include <span>

template<typename T, std::size_t Sz>

void printSpan(std::span<T, Sz> sp)

{

std::cout << '[' << sp.size() << " elems";

if constexpr (Sz == std::dynamic_extent) {

std::cout << " (dynamic)";

}

else {

std::cout << " (fixed)";

}

std::cout << ':';

for (const auto& elem : sp) {

std::cout << ' ' << elem;

}

std::cout << "]\n";

}

#endif // SPANPRINT_HPP

缺乏类型推导和转换也禁止将普通容器(如vector)传递给该函数。你需要明确指定类型或进行显式转换:

printSpan(vec);  // 错误:无法进行模板类型推导

printSpan(std::span{vec});  // 正确

printSpan<int, std::dynamic_extent>(vec);  // 正确(前提是vec是int类型的vector)

printSpan<int, 5>(vi);  //error: could not convert 'vi' from 'std::vector<int>' to 'std::span<int, 5>'

std::span<int, 5> span5{vi};

printSpan(span5);  //ok

你还可以考虑将元素类型声明为const:

template<typename T, std::size_t Sz>

void printSpan(std::span<const T, Sz> sp);

但是请注意,这样你就不能传递具有const元素类型的span。从非const类型到const类型的转换在模板中不会传播(有很好的原因)。

printSpan(std::span{vec});  // error

std::span<int, 5> span5{vi};

printSpan(span5);  //error

    因此,std::span<>不应该作为处理存储在连续内存中的元素序列的通用函数的词汇类型。

出于性能原因,你可以考虑像这样做:

template<typename T>

void print(const T& t) {

    if constexpr (std::ranges::contiguous_range<T> t) {

        processSpan<std::ranges::range_value_t<T>>(t);

    }

    else {

        // generic implementations for all containers/ranges

    }

}

使用span作为范围和视图

span是一个范围,可以在所有范围相关的算法和函数中使用。它甚至是一个借用的范围,这意味着你可以将临时的span作为范围传递给产生迭代器的算法:

std::vector<int> coll{25, 42, 2, 0, 122, 5, 7};

auto pos1 = std::ranges::find(std::span{coll.data(), 3}, 42); // 没有悬空迭代器

std::cout << *pos1 << '\n';

但是,请注意,如果span引用的是一个临时对象,这将是一个错误。下面的代码虽然编译通过,但会返回指向已销毁临时对象的迭代器:

auto pos2 = std::ranges::find(std::span{getData().data(), 3}, 42);

std::cout << *pos2 << '\n'; // 运行时错误

span也是一个视图并符合std::ranges::view的概念。

8.4 span的操作

这一部分详细介绍了span的类型和操作。

8.4.1 span 操作和成员类型概述

首先,列出所有不支持的操作:

• Comparisons (not even ==)

• swap()

• assign()

• at()

• I/O operators

• cbegin(), cend(), crbegin(), crend()

• Hashing

• Tuple-like API for structured bindings

也就是说,span既不是传统意义上的容器(在C++ STL中),也不是常规类型。

关于静态成员和成员类型,span提供了容器通常的成员(除了const_iterator)以及两个特殊的成员:element_type和extent(请参见《span的静态和类型成员表》)。

请注意,std::value_type并不是指定的元素类型(与std::array和其他一些类型的value_type通常不同)。它是移除了const和volatile修饰符后的元素类型。

构造std::span

只有在动态长度(extent)或长度(extent)为0时,才能使用默认构造函数创建一个 span:

std::span<int> sp1; // OK

std::span<int, 0> sp2; // OK

std::span<int, 5> sp3; // compile-time ERROR

如果这样创建一个 span 是有效的,那么 size() 的值为 0,data() 的值为 nullptr。

您还可以使用迭代器和长度,或者使用两个定义了有效范围的迭代器来创建和初始化 span,前提是这些迭代器引用连续的元素序列(std::contiguous_iterator 概念)。

std::vector<int> vec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

std::span<int> sp4a{vec}; // OK,引用所有元素

std::span<int> sp4b{vec.data(), vec.size()}; // OK,引用所有元素

std::span<int> sp4c{vec.begin(), vec.end()}; // OK,引用所有元素

std::span<int> sp4d{vec.data(), 5}; // OK,引用前5个元素

std::span<int> sp4e{vec.begin()+2, 5}; // OK,引用第3到第7个元素(包括)

std::list<int> lst{ 1,3,5,7,9 };

std::span<int> sp4f{lst.begin(), lst.end()}; // compile-time ERROR

如果 span 有固定的范围,它必须与传递的范围中的元素数量匹配。否则,它就是未定义的行为(可能工作,也可能不工作):

std::vector<int> vec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

std::span<int, 10> sp5a{vec}; // OK,引用所有元素

std::span<int, 5> sp5b{vec}; // runtime ERROR(未定义行为)

std::span<int, 20> sp5c{vec}; //runtime ERROR(未定义行为)

std::span<int, 5> sp5d{vec, 5}; // compile-time ERROR

std::span<int, 5> sp5e{vec.begin(), 5}; // OK,引用前5个元素

std::span<int, 3> sp5f{vec.begin(), 5}; // runtime ERROR(未定义行为)

std::span<int, 8> sp5g{vec.begin(), 5}; // runtime ERROR(未定义行为)

std::span<int, 5> sp5h{vec.begin()}; // ERROR

您还可以直接使用原始数组或 std::array 创建和初始化 span。在这种情况下,由于元素数量无效而导致的一些运行时错误将成为编译时错误:

int raw[10];

std::array arr{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

std::span<int> sp6a{raw}; // OK,引用所有元素

std::span<int> sp6b{arr}; // OK,引用所有元素

std::span<int, 5> sp6c{raw}; //compile-time ERROR

std::span<int, 5> sp6d{arr}; //compile-time ERROR

std::span<int, 5> sp6e{arr.data(), 5}; // 正常

换句话说:您可以将具有连续元素的容器作为整体传递,或者传递两个参数来指定初始元素范围。无论哪种情况,元素的数量必须与指定的固定范围匹配。

span 必须具有与其所引用的序列元素类型相同的元素类型。不支持类型转换(即使是隐式的标准转换)。然而,允许使用额外的限定符,如 const。这也适用于复制构造函数:

std::vector<int> vec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

std::span<const int> sp7a{vec}; // OK:具有 const 的元素类型

std::span<long> sp7b{vec}; // compile-time ERROR:无效的元素类型,类型不匹配

std::span<int> sp7c{sp7a}; //compile-time ERROR:移除 const 限定符

std::span<const long> sp7d{sp7a}; //compile-time ERROR:不同的元素类型

还支持类模板参数推导。当使用原始数组或 std::array<> 初始化 span 时,将推导出具有固定范围(etent)的 span。否则,它具有动态范围:

std::vector vec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 推导为 std::vector<int>

std::array arr{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 推导为 std::array<int, 10>

std::span sp8a{vec}; // OK:推导为 std::span<int>

std::span sp8b{arr}; // OK:推导为 std::span<int, 10>

std::span sp8c{arr.data(), 5}; // OK:推导为 std::span<int>

为了允许容器引用用户定义容器的元素,这些容器必须表明它们或它们的迭代器要求所有元素都在连续的内存中。为此,它们必须满足连续迭代器的概念。

构造函数还允许在 span 之间进行以下类型转换:

  • 具有固定范围的 span 可以转换为具有相同固定范围和额外限定符的 span。
  • 具有固定范围的 span 可以转换为具有动态范围的 span。
  • 具有动态范围的 span 可以转换为具有固定范围的 span,前提是当前范围适合。

http://www.kler.cn/a/585314.html

相关文章:

  • 【软考-架构】4.2、嵌入式软件-系统-RTOS-软件开发
  • 03.Python基础2
  • 【蓝桥杯集训·每日一题2025】 AcWing 4905. 面包店 python
  • Android LeakCanary使用与原理深度解析
  • R语言基础| 高级数据管理
  • mne溯源相关说明
  • ChatGPT、DeepSeek、Grok 三者对比:AI 语言模型的博弈与未来
  • RTSP/Onvif视频安防监控平台EasyNVR调用接口返回匿名用户名和密码的原因排查
  • Linux内核实时机制19 - RT调度器3 - 实时任务出入队
  • 【vLLM 学习】使用 TPU 安装
  • C++11 编译使用 aws-cpp-sdk
  • HTTP相关问题(AI回答)
  • 前端开发中的设计模式:装饰器模式的应用与实践
  • IDEA 一键完成:打包 + 推送 + 部署docker镜像
  • Python区块链应用开发从入门到精通
  • 深入理解 Python 中的进程池
  • leetcode203.移除链表元素
  • android 新闻客户端和springboot后台开发(一)
  • vue2:el-table列中文字前面加icon图标的两种方式
  • vue uniapp里照片多张照片展示