在C++20中,标准库引入了std::span,它是一个轻量级的不可变数组视图。相比传统的指针与长度组合,std::span提供了更安全、更直观的语义,使得函数间的数据共享变得简单且高效。本文将从std::span的基本概念入手,展示其在多种场景下的使用方法,并结合代码实例说明其性能优势。
1. std::span的核心概念
- 不可变性:
std::span本身不拥有数据,且对数据的修改取决于底层容器是否可写。它只能读写,但不会对内存进行管理。 - 可变长度:与数组不同,
std::span可以在运行时改变长度,只要保持底层数据不变。 - 安全性:在创建
std::span时,编译器会检查传入参数的类型和大小,避免了裸指针常见的错误。
#include <span>
#include <vector>
#include <array>
std::vector <int> vec = {1, 2, 3, 4, 5};
std::array<int, 3> arr = {10, 20, 30};
std::span <int> sv1(vec); // 从vector创建span
std::span <int> sv2(arr); // 从array创建span
2. std::span与函数参数
传统的C++函数往往使用指针与长度传递数组,但这会让调用者必须手动维护长度,且容易出现越界。使用std::span可以让接口更简洁且更安全。
void process(std::span <int> data) {
for (auto v : data) {
// 这里可以安全访问 data[0] ~ data[data.size()-1]
}
}
int main() {
std::vector <int> vec = {1, 2, 3};
process(vec); // 自动转换为span
}
3. std::span的子视图(subspan)
std::span支持切片操作,返回新的span,不复制数据。
std::span <int> full{vec};
auto sub = full.subspan(1, 2); // 从索引1开始,长度2
4. 与STL算法的配合
大多数STL算法已经接受std::span作为迭代器区间的一种形式,或者可以通过std::begin和std::end得到迭代器。
std::span <int> data = vec;
std::sort(data.begin(), data.end()); // 直接使用span的begin/end
5. 性能比较
与裸指针+长度相比,std::span的调用成本相同,但提供了类型安全。与std::vector直接传参相比,std::span避免了额外的复制或引用计数开销。
实验代码
#include <iostream>
#include <vector>
#include <span>
#include <chrono>
void sum_ptr(const int* arr, std::size_t n, long long& out) {
long long sum = 0;
for (std::size_t i = 0; i < n; ++i) sum += arr[i];
out = sum;
}
void sum_span(std::span<const int> s, long long& out) {
long long sum = 0;
for (auto v : s) sum += v;
out = sum;
}
int main() {
const std::size_t N = 100'000'000;
std::vector <int> data(N, 1);
long long result = 0;
auto t1 = std::chrono::high_resolution_clock::now();
sum_ptr(data.data(), data.size(), result);
auto t2 = std::chrono::high_resolution_clock::now();
auto t3 = std::chrono::high_resolution_clock::now();
sum_span(std::span<const int>(data), result);
auto t4 = std::chrono::high_resolution_clock::now();
std::cout << "ptr: " << std::chrono::duration<double>(t2-t1).count() << "s\n";
std::cout << "span: " << std::chrono::duration<double>(t4-t3).count() << "s\n";
}
运行结果显示,两种实现时间相差极小,几乎可以忽略,证明std::span在性能上几乎无额外成本。
6. 常见误区
- 误以为
std::span会复制数据:span只是一段视图,数据仍由原始容器持有。 - 使用未初始化的span:如果创建空的`std::span s;`,其`size()`为0,但内部指针是未定义的,切勿解引用。
- 跨越生命周期:如果
span引用的容器被销毁,span将悬空。使用时务必确保生命周期一致。
7. 进阶使用:可变span与子span
std::span可以是可变的(`std::span