span
std::span
是 C++20 引入的一个非常实用的特性,它提供了一个非拥有 (non-owning) 的、**连续内存区域的视图 (view)**。你可以把它理解为 std::string_view
的泛化版本,std::string_view
是 char
序列的非拥有视图,而 std::span
是任意类型 T
的序列的非拥有视图。
std::span
的核心思想和解决的问题
在 C++20 之前,我们经常需要处理连续内存区域,例如:
- C 风格数组 (
int arr[10];
) std::vector
std::array
- 其他自定义的连续内存容器
当我们需要将这些数据传递给函数时,通常有几种方式:
- 原始指针和长度:
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. 从 std::vector
初始化
std::vector
可以隐式转换为 std::span
。
1 | std::vector<int> vec = {10, 20, 30}; |
3. 从 std::array
初始化
std::array
也可以隐式转换为 std::span
。
1 | std::array<double, 4> arr = {1.1, 2.2, 3.3, 4.4}; |
4. 从指针和长度初始化
1 | int* ptr = new int[3]{100, 200, 300}; |
5. 从迭代器对初始化 (C++23 以后更直接,C++20 可以通过 std::ranges::subrange
或 std::span
构造函数)
C++20 的 std::span
构造函数可以直接接受一对迭代器,只要它们满足 std::contiguous_iterator
概念。
1 | std::vector<int> another_vec = {1, 2, 3, 4, 5, 6}; |
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 |
|
std::span
的优势
- 类型安全和边界检查 (可选): 虽然
operator[]
不做边界检查,但std::span
本身携带了长度信息,你可以轻松地在函数内部进行边界检查,或者使用gsl::at
等工具。相比于裸指针,它提供了更多的信息。 - 避免数据复制: 作为视图,它不拥有数据,因此在函数调用时避免了不必要的数据复制,提高了性能。
- 统一接口: 无论是 C 风格数组、
std::vector
还是std::array
,都可以通过std::span
提供统一的接口。 - 清晰的意图: 函数签名
void func(std::span<T> s)
明确表达了“我需要一个连续的T
序列,但我不会拥有它或管理它的生命周期”。 - 支持
constexpr
: 可以在编译时进行操作,例如创建固定大小的 span。
std::span
的重要注意事项 (悬空视图)
由于 std::span
是非拥有的,它不会延长其所指向数据的生命周期。这是使用 std::span
时最需要注意的地方。如果原始数据被销毁,而 std::span
仍然存在并被访问,就会导致未定义行为。
1 |
|
最佳实践:
- 确保
std::span
的生命周期短于或等于它所指向数据的生命周期。 - 通常将
std::span
用作函数参数,而不是作为类成员变量(除非你非常清楚其生命周期管理)。 - 对于只读访问,优先使用
std::span<const T>
。
总结
std::span
是现代 C++ 中处理连续内存数据不可或缺的工具。它通过提供一个轻量级、类型安全、非拥有的视图,极大地提升了代码的安全性、可读性和性能。在 C++20 及更高版本中,你应该优先考虑在需要传递连续序列的场景中使用 std::span
来替代原始指针/长度对或 const std::vector<T>&
。但是,务必牢记其非拥有的特性,避免创建悬空视图。