std::span 是 C++20 引入的一个非常实用的特性,它提供了一个非拥有 (non-owning) 的、**连续内存区域的视图 (view)**。你可以把它理解为 std::string_view 的泛化版本,std::string_viewchar 序列的非拥有视图,而 std::span 是任意类型 T 的序列的非拥有视图。

std::span 的核心思想和解决的问题

在 C++20 之前,我们经常需要处理连续内存区域,例如:

  1. C 风格数组 (int arr[10];)
  2. std::vector
  3. std::array
  4. 其他自定义的连续内存容器

当我们需要将这些数据传递给函数时,通常有几种方式:

  • 原始指针和长度: void func(int* data, size_t size); 这种方式容易出错,容易忘记传递长度,或者长度与实际数据不匹配,导致越界访问。
  • const std::vector<T>& 如果函数只读取数据,这种方式比较安全。但如果函数只需要一部分数据,或者数据来源不是 std::vector,就显得不那么通用。而且,它传递了整个 vector 对象,虽然是引用,但接口上表达的意图是“我可能需要整个 vector”,而实际上可能只需要其数据。
  • 迭代器对: void func(Iterator begin, Iterator end); 这种方式很灵活,但写起来比较冗长,且不直接表达“连续内存”的语义。

std::span 旨在解决上述问题,它提供了一个统一、安全、高效的方式来表示和传递任何连续内存区域。

核心特点:

  • 非拥有 (Non-owning): std::span 不管理它所指向的内存的生命周期。它只是一个“视图”,就像一个窗口,透过它可以看到一块内存。这意味着,如果 std::span 所指向的原始数据被销毁了,那么 std::span 就会变成一个**悬空视图 (dangling span)**,访问它将是未定义行为。
  • 连续内存 (Contiguous memory): std::span 只能指向内存中连续存储的数据。
  • 轻量级 (Lightweight): std::span 通常只包含一个指向数据开头的指针和一个表示数据长度的 size_t,所以它的复制和传递成本非常低。
  • 类型安全 (Type-safe): 它是一个模板类,会检查类型匹配。
  • 支持 constexpr 可以在编译时使用。

std::span 的声明和初始化

std::span 是一个模板类,通常声明为 std::span<T>std::span<T, Extent>

  • std::span<T>:动态大小的 span,其长度在运行时确定。
  • std::span<T, Extent>:固定大小的 span,其长度在编译时确定。Extent 必须是一个非负整数。

1. 从 C 风格数组初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <span> // C++20
#include <vector>
#include <array>

void print_span(std::span<const int> s) { // 接收 const span,表示只读
std::cout << "Span size: " << s.size() << ", elements: ";
for (int val : s) {
std::cout << val << " ";
}
std::cout << std::endl;
}

int main() {
int c_array[] = {1, 2, 3, 4, 5};
std::span<int> s1(c_array); // 从 C 风格数组初始化 (动态大小)
print_span(s1); // 输出: Span size: 5, elements: 1 2 3 4 5

std::span<int, 5> s2(c_array); // 从 C 风格数组初始化 (固定大小)
print_span(s2); // 输出: Span size: 5, elements: 1 2 3 4 5

// 注意:固定大小的 span 必须与数组大小匹配,否则编译错误
// std::span<int, 4> s3(c_array); // 编译错误
}

2. 从 std::vector 初始化

std::vector 可以隐式转换为 std::span

1
2
3
4
5
6
std::vector<int> vec = {10, 20, 30};
std::span<int> s3(vec); // 从 std::vector 初始化 (动态大小)
print_span(s3); // 输出: Span size: 3, elements: 10 20 30

// 也可以直接传递给接受 std::span 的函数
print_span(vec); // 隐式转换

3. 从 std::array 初始化

std::array 也可以隐式转换为 std::span

1
2
3
4
5
6
7
8
std::array<double, 4> arr = {1.1, 2.2, 3.3, 4.4};
std::span<const double> s4(arr); // 从 std::array 初始化
// 注意:print_span 接受 int,这里需要一个接受 double 的函数
std::cout << "Span double size: " << s4.size() << ", elements: ";
for (double val : s4) {
std::cout << val << " ";
}
std::cout << std::endl; // 输出: Span double size: 4, elements: 1.1 2.2 3.3 4.4

4. 从指针和长度初始化

1
2
3
4
int* ptr = new int[3]{100, 200, 300};
std::span<int> s5(ptr, 3); // 从指针和长度初始化
print_span(s5); // 输出: Span size: 3, elements: 100 200 300
delete[] ptr;

5. 从迭代器对初始化 (C++23 以后更直接,C++20 可以通过 std::ranges::subrangestd::span 构造函数)

C++20 的 std::span 构造函数可以直接接受一对迭代器,只要它们满足 std::contiguous_iterator 概念。

1
2
3
4
std::vector<int> another_vec = {1, 2, 3, 4, 5, 6};
// 创建一个包含 vec 中间元素的 span
std::span<int> s6(another_vec.begin() + 1, another_vec.begin() + 4); // 元素 2, 3, 4
print_span(s6); // 输出: Span size: 3, elements: 2 3 4

std::span 的常用成员函数

  • size(): 返回 span 中的元素数量。
  • empty(): 检查 span 是否为空。
  • data(): 返回指向 span 第一个元素的指针。
  • operator[]: 访问指定索引的元素(不进行边界检查)。
  • front(): 访问第一个元素(不进行边界检查)。
  • back(): 访问最后一个元素(不进行边界检查)。
  • begin(), end(): 返回迭代器,支持范围 for 循环。
  • subspan(offset, count): 返回一个子 span。
    • offset: 子 span 起始位置的偏移量。
    • count: 子 span 的元素数量(可选,默认为从偏移量到末尾的所有元素)。
  • first(count): 返回包含前 count 个元素的子 span。
  • last(count): 返回包含后 count 个元素的子 span。
  • as_bytes(): 返回一个 std::span<const std::byte>,用于查看原始字节数据。
  • as_writable_bytes(): 返回一个 std::span<std::byte>,用于修改原始字节数据。

示例:成员函数使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include <span>
#include <vector>
#include <numeric> // For std::iota
#include <cstddef> // For std::byte

void modify_span(std::span<int> s) { // 接收可写 span
for (int& val : s) {
val *= 2;
}
}

int main() {
std::vector<int> data(10);
std::iota(data.begin(), data.end(), 1); // data: {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

std::span<int> full_span(data);
print_span(full_span); // 1 2 3 4 5 6 7 8 9 10

// 访问元素
std::cout << "First element: " << full_span.front() << std::endl; // 1
std::cout << "Element at index 3: " << full_span[3] << std::endl; // 4

// 创建子 span
std::span<int> middle_three = full_span.subspan(3, 3); // 从索引 3 开始,取 3 个元素
print_span(middle_three); // 4 5 6

std::span<int> first_two = full_span.first(2);
print_span(first_two); // 1 2

std::span<int> last_four = full_span.last(4);
print_span(last_four); // 7 8 9 10

// 修改 span 的内容 (会修改原始数据)
modify_span(middle_three); // 修改 {4, 5, 6} 为 {8, 10, 12}
print_span(full_span); // 1 2 3 8 10 12 7 8 9 10 (注意原始 data 被修改了)

// as_bytes 和 as_writable_bytes
std::cout << "Raw bytes of first_two: ";
for (const std::byte b : first_two.as_bytes()) {
std::cout << std::hex << static_cast<int>(b) << " ";
}
std::cout << std::dec << std::endl; // 输出原始字节表示

// 创建一个空的 span
std::span<int> empty_span;
std::cout << "Empty span size: " << empty_span.size() << std::endl; // 0
}

std::span 的优势

  1. 类型安全和边界检查 (可选): 虽然 operator[] 不做边界检查,但 std::span 本身携带了长度信息,你可以轻松地在函数内部进行边界检查,或者使用 gsl::at 等工具。相比于裸指针,它提供了更多的信息。
  2. 避免数据复制: 作为视图,它不拥有数据,因此在函数调用时避免了不必要的数据复制,提高了性能。
  3. 统一接口: 无论是 C 风格数组、std::vector 还是 std::array,都可以通过 std::span 提供统一的接口。
  4. 清晰的意图: 函数签名 void func(std::span<T> s) 明确表达了“我需要一个连续的 T 序列,但我不会拥有它或管理它的生命周期”。
  5. 支持 constexpr 可以在编译时进行操作,例如创建固定大小的 span。

std::span 的重要注意事项 (悬空视图)

由于 std::span 是非拥有的,它不会延长其所指向数据的生命周期。这是使用 std::span 时最需要注意的地方。如果原始数据被销毁,而 std::span 仍然存在并被访问,就会导致未定义行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <span>
#include <vector>

std::span<int> create_dangling_span() {
std::vector<int> temp_vec = {1, 2, 3};
// temp_vec 在函数返回时会被销毁
return std::span<int>(temp_vec); // 返回一个指向已销毁内存的 span
}

int main() {
std::span<int> s = create_dangling_span();
// 此时 s 是一个悬空视图!访问它会导致未定义行为。
// 编译器通常不会警告你这种错误。
// std::cout << s[0] << std::endl; // 危险!
return 0;
}

最佳实践:

  • 确保 std::span 的生命周期短于等于它所指向数据的生命周期。
  • 通常将 std::span 用作函数参数,而不是作为类成员变量(除非你非常清楚其生命周期管理)。
  • 对于只读访问,优先使用 std::span<const T>

总结

std::span 是现代 C++ 中处理连续内存数据不可或缺的工具。它通过提供一个轻量级、类型安全、非拥有的视图,极大地提升了代码的安全性、可读性和性能。在 C++20 及更高版本中,你应该优先考虑在需要传递连续序列的场景中使用 std::span 来替代原始指针/长度对或 const std::vector<T>&。但是,务必牢记其非拥有的特性,避免创建悬空视图。