C++20 中的 std::span:轻量级数组视图的使用与实践

在 C++20 之前,处理数组或者容器切片时常常需要自己编写指针和长度的组合,或者使用标准库提供的 std::arraystd::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::spanstd::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++ 代码。

发表评论