在 C++20 之前,处理数组或者容器切片时常常需要自己编写指针和长度的组合,或者使用标准库提供的 std::array、std::vector 以及自定义的包装类。C++20 引入了 std::span,它是一个无所有权、轻量级的数组视图,极大地简化了对连续内存块的访问和操作。
1. std::span 的基本定义
#include <span>
#include <iostream>
#include <vector>
#include <array>
int main() {
std::array<int, 10> arr = {0,1,2,3,4,5,6,7,8,9};
std::span <int> sp1(arr); // 视图整个数组
std::span <int> sp2(arr.begin() + 3, 4); // 视图子范围 [3,6]
}
std::span<T, Extent> 的 Extent 可以是固定大小(模板参数),也可以是动态大小(std::dynamic_extent)。若是动态大小,span 只包含指向起始元素的指针和长度。
2. 与指针和迭代器的对比
- 指针:只能提供起始地址,长度信息必须单独管理,容易出错。
- 迭代器:可用于遍历,但不一定能提供长度,且对数组视图不够直观。
- span:既有起始指针,又有长度,且是纯粹的“视图”,没有所有权,使用非常安全。
3. 常见使用场景
3.1 作为函数参数
void process(std::span <int> data) {
for (auto& x : data) x *= 2;
}
int main() {
std::vector <int> vec = {1,2,3,4};
process(vec); // 自动转换为 std::span <int>
process(vec.data(), vec.size()); // 旧式写法
}
3.2 与 STL 算法配合
#include <algorithm>
#include <numeric>
std::span<const double> scores = {0.9, 0.7, 0.8, 0.6};
double avg = std::accumulate(scores.begin(), scores.end(), 0.0) / scores.size();
auto maxIt = std::max_element(scores.begin(), scores.end());
3.3 切片与子视图
std::span <int> full = arr; // 整个数组
std::span <int> sub = full.subspan(2, 5); // 从索引 2 开始,长度 5 的子视图
4. 安全性与异常安全
std::span不会复制元素,也不负责生命周期管理,使用时必须保证底层数据在span生命周期内有效。- 由于不拥有数据,异常不需要担心资源泄露,异常安全级别与裸指针相同。
- 典型错误:将
std::span传递给返回指向原始容器的数据成员的函数,导致悬空引用。使用span时请确认容器不会被销毁或修改容量。
5. 与 std::array / std::vector 的区别
| 属性 | std::array | std::vector | std::span |
|---|---|---|---|
| 所有权 | 是 | 是 | 否 |
| 大小 | 编译时固定 | 动态 | 动态 |
| 适用场景 | 固定长度容器 | 可变长度容器 | 视图 / 切片 |
| 内存占用 | 需要存储元素 | 需要存储指针、大小、容量 | 只存储指针和长度 |
6. 进阶:std::span 与 std::span_view
C++23 引入了 std::span_view,它允许在 std::span 上进行链式切片而不产生额外的 span 对象,进一步提升性能。
std::span_view <int> sv = arr; // 与 std::span 等价,但不包含指针
auto sub = sv.subspan(5, 3); // 子视图
7. 小结
std::span 的出现大大简化了 C++ 代码中的数组切片、函数参数传递以及与 STL 算法的配合。它保持了对底层数据的无所有权特性,既安全又轻量。熟练使用 span,可以写出更简洁、易读、易维护的 C++ 代码。