在 C++20 标准中,std::span 被引入为一个轻量级的视图对象,用来描述一段连续的内存。它不拥有数据,只是提供对已有数组、容器或裸指针的统一接口,使得函数可以同时处理 C 风格数组、std::array、std::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. 子视图的强大功能
span 的 subspan、first、last 可以非常方便地实现切片、窗口、滑动窗口等常见算法。
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::array、std::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::span 的 data() 直接写入 |
可能修改外部数据 | 如果不想修改,可使用 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是一种非拥有、轻量级的视图对象,专门用于引用连续内存块。- 它让函数接口统一化,减少模板代码冗余。
subspan、first、last提供了强大的子视图能力,适用于滑动窗口、矩阵分块等算法。- 关键是管理好底层数据的生命周期,避免悬空引用。
- 与
std::ranges结合,可进一步提高代码表达力。
掌握 std::span 后,你将能够在 C++20 代码中写出更简洁、更安全、更高效的接口与算法。