如何在 C++20 中使用 std::span 进行安全数组遍历?

在 C++20 中引入的 std::span 为数组、容器和裸数组提供了一种轻量级、无所有权的视图,使得安全遍历和切片操作变得异常简洁。下面从概念、使用场景、典型代码以及性能与安全性四个维度进行深入剖析,帮助你在实际项目中高效、可靠地使用 std::span。

1. 什么是 std::span?

std::span 是一个模板类,用来描述一段连续存储区(如数组、std::vector、std::array、裸数组等)的非所有权视图。它包含两个核心成员:

  • T* data_:指向首元素的指针。
  • size_t size_:元素数量(Extent 可为动态或固定值)。

与 std::vector、std::array 等拥有所有权的容器不同,span 只提供对已有存储的访问,不负责内存分配或释放,使用更安全、轻量。

2. 典型使用场景

场景 说明 示例
函数参数 接受数组、容器等任意连续区块 `void process(span
data)`
切片 在不复制数据的前提下截取子段 auto sub = full_span.subspan(5, 10);
统一接口 对多种容器提供统一的遍历方式 for (auto v : span(container)) …
安全边界检查 通过 size() 进行范围检查,避免越界 if (idx < data.size()) …

3. 基础用法示例

#include <iostream>
#include <span>
#include <vector>
#include <array>

void print(span <int> s) {
    for (int v : s) std::cout << v << ' ';
    std::cout << '\n';
}

int main() {
    int arr[5] = {1,2,3,4,5};
    vector <int> vec = {10,20,30,40,50,60};

    // 直接传递裸数组
    print(arr);

    // 传递 vector 的 span
    print(span <int>(vec));

    // 传递 std::array
    array<int,4> a{100,200,300,400};
    print(a);

    // 切片示例
    auto sub = vec.subspan(2,3); // 取 vec[2],vec[3],vec[4]
    print(sub);
}

运行结果:

1 2 3 4 5 
10 20 30 40 50 60 
100 200 300 400 
30 40 50 

重要细节

  1. 构造

    • span<T, N> 可通过 span<T> s{arr}span<T> s{arr, N} 初始化。
    • std::vectorstd::array 直接隐式构造。
    • 对裸数组 int* + size 亦可构造。
  2. 子切片

    • subspan(offset) 返回从 offset 开始、到结尾的视图。
    • subspan(offset, length) 返回指定长度的视图。
    • offset + length > size(),会 throw std::out_of_range
  3. 空视图

    • `span ()` 创建空视图。
    • `span empty{}` 亦可。

4. 与传统指针和引用的对比

方式 优点 缺点
原始指针 + 长度 简洁 易出错,缺乏边界检查
std::array / std::vector 拥有所有权 不适合只需要读访问时的无所有权需求
std::span 轻量、无所有权、边界检查 仅支持连续内存

5. 性能考量

  • 无拷贝:span 本身仅保存指针和长度,复制成本极低(两个指针)。
  • 对齐与缓存:与裸指针等效,编译器可进行同等级别的优化。
  • ABI 兼容:span 的 ABI 与结构体相同,能与 C 接口无缝对接。

6. 安全性与常见陷阱

  1. 悬空引用

    • span 指向的底层数据若在 span 生命周期内被销毁,使用将导致未定义行为。
    • 解决方案:保证 span 生命周期不超过底层数据。
  2. 长度超界

    • subspan 需要手动检查范围,否则抛异常。
    • 推荐在调用前先用 size() 验证。
  3. 空指针

    • span 可以合法持有空指针(如 `span {nullptr, 0}`)。
    • 避免对 data() 的解引用,先检查 empty()

7. 与 C++23 迭代器的整合

C++23 对 span 引入了更丰富的迭代器支持(如 span::begin()span::end()),可以直接使用标准算法:

#include <algorithm>
auto s = span <int>{vec};
std::sort(s.begin(), s.end()); // 对原 vector 进行排序

8. 小结

std::span 在 C++20 里是一个极具价值的工具,它把“视图”概念与容器无缝结合。通过使用 span:

  • 代码更简洁、可读性更高。
  • 函数接口更通用,支持多种容器。
  • 同时提供安全边界检查,降低越界风险。
  • 性能几乎不受影响,接近裸指针级别。

掌握 std::span 的使用,你的 C++ 代码将在接口设计、性能与安全性之间实现更优的平衡。

发表评论