在 C++20 中新增的 std::span 为程序员提供了一种安全、高效且易于使用的数组视图。它不像 std::vector 那样拥有自己的存储空间,而是对已有的连续内存块(如 C 风格数组、std::vector、std::array 或自定义容器)提供一个不拷贝、不复制的视图。下面我们从概念、实现、典型使用场景、常见陷阱以及最佳实践等角度,详细拆解 std::span。
1. std::span 的基本概念
std::span<T, Extent> 定义在 <span> 头文件中。它由两部分构成:
- 指针:指向数据块的首地址。
- 长度:该视图中元素的数量。
因为它不持有所有权,std::span 仅在其所引用的数据有效期间有效。其核心成员函数:
size()/size_bytes():返回元素个数 / 字节数。empty():是否为空。data():获取底层指针。operator[]:随机访问。front()/back():访问首尾元素。begin()/end():迭代器。
此外,std::span 提供 subspan() 方法,返回原 span 的子视图,支持范围检查。
2. 实现细节与性能
std::span 仅包含两个成员(指针和长度),其尺寸与 std::array 相同。构造、赋值、拷贝和移动都是极其轻量级的 O(1) 操作,几乎不产生额外开销。
注意:
std::span不是线程安全的,它只是一个裸视图。多线程访问时,需要自行同步。
3. 典型使用场景
3.1 作为函数参数
传统 C++ 中,往往需要重载函数以支持数组、指针和 vector。std::span 可以一次性解决这一痛点。
#include <span>
#include <vector>
#include <array>
#include <iostream>
void print(span<const int> sp) {
for (int v : sp) std::cout << v << ' ';
std::cout << '\n';
}
int main() {
int arr[] = {1,2,3};
std::vector <int> vec = {4,5,6};
std::array<int,3> arr2 = {7,8,9};
print(arr); // 自动推导为 std::span <int>
print(vec); // 隐式转换
print(arr2); // 同上
}
3.2 统一处理 C 风格数组与 STL 容器
void process(span<const double> data) {
// 例如计算平均值
double sum = 0;
for (double d : data) sum += d;
std::cout << "avg = " << sum / data.size() << '\n';
}
3.3 与标准算法配合
标准算法接受任何满足 ForwardIterator 的范围。由于 std::span 的 begin() / end() 满足这一条件,可以直接使用:
std::sort(mySpan.begin(), mySpan.end()); // 只会对视图范围内的数据进行排序
3.4 作为临时视图
有时我们只想在临时表达式中访问某段数组,而不想创建临时容器。std::span 的 subspan 能轻松做到。
std::vector <int> v = {1,2,3,4,5,6,7,8,9,10};
auto middle = std::span(v).subspan(3, 4); // 视图 [4,5,6,7]
4. 常见陷阱
-
生命周期管理
std::span只是一种视图,所引用的数据若在 span 之外被销毁或重新分配,使用该 span 将导致悬挂指针。始终确保数据的生命周期覆盖整个 span 的使用期。 -
非连续存储
std::span只能视图连续内存。如果你想对非连续容器(如std::list)进行类似操作,必须先复制到连续容器。 -
类型安全
` 创建 `span` 并解引用,导致未定义行为。必须保证类型匹配。
std::span的模板参数是元素类型。若从 `std::vector -
对
Extent的误用
std::span<int, 10>需要 10 个元素;但在使用时传入长度不匹配会抛出std::out_of_range。在subspan或first/last时要注意长度检查。
5. 与旧标准的兼容性
- 在 C++17 之前,可以使用 Boost 的
boost::span或gsl::span(Guidelines Support Library)实现类似功能。 - 若项目已采用 C++20 或之后的标准,推荐直接使用标准库
std::span。
6. 进阶技巧
6.1 与 std::span 结合自定义容器
template<typename T>
class MyBuffer {
std::unique_ptr<T[]> data_;
size_t size_;
public:
MyBuffer(size_t sz) : data_(new T[sz]), size_(sz) {}
std::span <T> span() { return std::span<T>(data_.get(), size_); }
// 其它成员...
};
6.2 使用 std::span 作为回调参数
void compute(span <double> buffer, double factor) {
for (auto& val : buffer) val *= factor;
}
6.3 与 std::array_view 或 std::string_view 的区别
std::span适用于任何类型的对象。std::string_view专门用于char/wchar_t等字符序列,并且没有长度信息。
7. 小结
- 轻量且安全:
std::span只存储指针和长度,使用时几乎无性能损失。 - 广泛兼容:可接收数组、
std::vector、std::array甚至自定义容器。 - 生命周期警告:始终确保被视图的数据在 span 存在期间不被销毁或重新分配。
- 功能丰富:提供
subspan、迭代器、随机访问等,天然可配合 STL 算法。
在现代 C++ 开发中,std::span 已经成为函数接口设计的标准工具之一。熟练掌握它的使用,能让代码更简洁、意图更明确,并避免常见的数组指针相关错误。祝你在项目中愉快地使用 std::span!