如何在 C++20 中使用 std::span 处理数组片段

在 C++20 标准中,std::span 被引入为一个轻量级的视图对象,用来描述一段连续的内存。它不拥有数据,只是提供对已有数组、容器或裸指针的统一接口,使得函数可以同时处理 C 风格数组、std::arraystd::vector 等不同容器,而无需额外的复制或模板特化。下面我们从基本使用到高级技巧,系统阐述 std::span 的设计理念、典型用法以及如何在项目中安全高效地使用它。

1. std::span 的基本定义

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

std::span 是一个类模板,定义为:

template<class ElementType, std::size_t Extent = std::dynamic_extent>
class span;
  • ElementType:元素的类型,必须是 完整类型
  • Extent:数组的长度。如果是 std::dynamic_extent(默认值),则长度是动态的;若给定具体数值,则 span 只能引用具有该长度的数组。

常用的构造方式:

std::array<int, 5> a = {1, 2, 3, 4, 5};
std::vector <int> v = {10, 20, 30, 40, 50, 60};

std::span <int> s1(a);      // 自动推断长度为 5
std::span <int> s2(v);      // 推断长度为 6
std::span <int> s3(a.data(), 3); // 指向前三个元素

2. std::span 的核心成员

成员 描述
size() 返回当前 span 的元素数量
empty() 判断是否为空
data() 返回指向第一个元素的指针
operator[] 访问指定索引的元素
begin()/end() 返回迭代器,支持范围 for
subspan(pos, count) 创建从 pos 开始、长度为 count 的子 span
first(count) / last(count) 创建长度为 count 的前/后子 span
empty() 判断是否为空

注意span 并不负责内存管理,使用时一定要确保底层数据的生命周期至少与 span 的使用时间相同。

3. 典型场景:函数接口的统一

3.1 传统做法

void processArray(const int* arr, std::size_t n);
void processVector(const std::vector <int>& v);

两种不同的接口导致调用者需要为不同容器编写两套代码,且重复的长度参数容易出错。

3.2 span 方案

void processSpan(std::span<const int> s) {
    for (int x : s) {
        std::cout << x << ' ';
    }
}

调用:

processSpan(a);   // std::array
processSpan(v);   // std::vector
processSpan(a.data(), 3); // 前 3 个元素
processSpan(&a[2], 2); // 从 a[2] 开始的 2 个元素

这样,单一函数即可兼容所有连续存储的容器,代码更简洁、易维护。

4. 子视图的强大功能

spansubspanfirstlast 可以非常方便地实现切片、窗口、滑动窗口等常见算法。

std::vector <int> buf = {1, 2, 3, 4, 5, 6, 7, 8, 9};
std::span <int> window(buf);

for (std::size_t i = 0; i + 3 <= window.size(); ++i) {
    std::span <int> win = window.subspan(i, 3); // 3 个元素的窗口
    // 处理窗口
    std::cout << "窗口 " << i << ": ";
    for (int x : win) std::cout << x << ' ';
    std::cout << '\n';
}

4.1 递归子窗口

如果需要更复杂的窗口分解,例如把数组按 2 维切分,可以使用 subspan 结合 span::size() 计算。

void processGrid(std::span <int> grid, std::size_t cols) {
    std::size_t rows = grid.size() / cols;
    for (std::size_t r = 0; r < rows; ++r) {
        auto row = grid.subspan(r * cols, cols);
        // 处理每一行
    }
}

5. 与 std::arraystd::vector 的互操作

  • std::array<T, N> 可以直接构造为 span<T, N>
  • `std::vector ` 在 `std::span` 中会自动推断 `Extent = std::dynamic_extent`。
  • 由于 span 只持有指针和长度,向 std::span 传递 std::vector 并不会拷贝整个容器。

6. 安全性与陷阱

场景 风险 解决方案
对局部数组返回 span 数组生命周期结束后引用悬空 永远不要把栈数组的 span 返回到外部函数
对非连续容器(如链表)使用 span 数据不连续 仅适用于连续内存容器
多线程共享同一 span 可能出现数据竞争 确保对底层数据的访问是线程安全的
std::spandata() 直接写入 可能修改外部数据 如果不想修改,可使用 std::span<const T>std::span<T const>

7. 进阶:与 std::ranges 的结合

C++20 的 std::ranges 允许我们对 span 进行更复杂的视图操作,例如 views::reverse, views::filter 等。

#include <ranges>

void printReversed(std::span<const int> s) {
    for (int x : s | std::views::reverse) {
        std::cout << x << ' ';
    }
    std::cout << '\n';
}

这种组合让我们既能利用 span 的轻量级特点,又能享受 ranges 的表达力。

8. 小结

  • std::span 是一种非拥有、轻量级的视图对象,专门用于引用连续内存块。
  • 它让函数接口统一化,减少模板代码冗余。
  • subspanfirstlast 提供了强大的子视图能力,适用于滑动窗口、矩阵分块等算法。
  • 关键是管理好底层数据的生命周期,避免悬空引用。
  • std::ranges 结合,可进一步提高代码表达力。

掌握 std::span 后,你将能够在 C++20 代码中写出更简洁、更安全、更高效的接口与算法。

发表评论