3.4、std::span视图
什么是std::span
std::span
本质上是一个轻量级的非所有权视图,用于表示一段连续内存区域中的对象序列。它定义在<span>
头文件中,设计目标是替代传统的指针+长度组合,提供更安全、更富表现力的接口。
简单来说,std::span
是:
- • 一个非拥有性视图:它只是对已有数据的引用,不负责管理这些数据的生命周期
- • 只能引用连续序列:如数组、
std::vector
或std::array
中的元素 - • 可以具有静态范围(编译时已知长度)或动态范围(运行时确定长度)
- • 一个零开销抽象:编译器能对其进行优化,消除不必要的开销
为什么我们需要span
C语言留给我们的最大遗产之一就是数组退化为指针的问题。当数组作为函数参数传递时,关于其大小的信息丢失了,导致了无数的安全漏洞和程序错误。
思考这个典型的C风格函数:
void process_array(int* arr, size_t length);
这种设计存在几个明显问题:
- • 编译器无法检查传入的
length
是否与实际数组大小匹配 - • 调用时必须分别传递数组和大小,容易出错
- • 无法在使用时区分是单个元素的指针还是数组的开始
std::span
优雅地解决了这些问题,提供了一种安全且高效的方式来引用连续内存区域。
基本用法详解
创建span
#include <span>
#include <vector>
#include <array>
#include <iostream>
int main() {
// 从原始数组创建
int arr[] = {1, 2, 3, 4, 5};
std::span<int> sp1{arr}; // 动态范围,大小在运行时确定
// 从vector创建
std::vector<int> vec{10, 20, 30, 40, 50};
std::span<int> sp2{vec};
// 从数组部分区间创建
std::span<int> sp3{arr, 3}; // 只包含前3个元素
// 创建静态范围的span(编译时已知大小)
std::span<int, 5> sp4{arr}; // 必须正好有5个元素
// 打印span大小
std::cout << "sp1 size: " << sp1.size() << std::endl;
std::cout << "sp2 size: " << sp2.size() << std::endl;
std::cout << "sp3 size: " << sp3.size() << std::endl;
std::cout << "sp4 size: " << sp4.size() << std::endl;
}
常用操作
std::span
提供了丰富的接口来操作内存区域:
#include <span>
#include <vector>
#include <iostream>
void print_span(std::span<const int> sp) {
for (const auto& elem : sp) {
std::cout << elem << " ";
}
std::cout << std::endl;
}
int main() {
std::vector<int> vec{10, 20, 30, 40, 50};
std::span<int> sp{vec};
// 元素访问
std::cout << "第一个元素: " << sp.front() << std::endl;
std::cout << "最后一个元素: " << sp.back() << std::endl;
std::cout << "第三个元素: " << sp[2] << std::endl;
// 检查是否为空
std::cout << "span 是否为空: " << std::boolalpha << sp.empty() << std::endl;
// 创建子视图
auto first3 = sp.first(3); // 前3个元素
auto last3 = sp.last(3); // 后3个元素
auto mid3 = sp.subspan(1, 3); // 从索引1开始的3个元素
std::cout << "前3个元素: ";
print_span(first3);
std::cout << "后3个元素: ";
print_span(last3);
std::cout << "中间3个元素: ";
print_span(mid3);
// 修改元素(原始数据也会改变)
sp[0] = 100;
std::cout << "修改后的vector: " << vec[0] << std::endl;
}
深入解析与高级用法
静态范围vs动态范围
std::span
有两种类型:固定大小(静态范围)和可变大小(动态范围)。
template<typename ElementType, size_t Extent = dynamic_extent>
class span;
静态范围的span
在编译时就知道其大小,可以让编译器进行更多优化。当你确切知道视图大小时,应优先使用静态范围:
std::array<int, 5> arr{1, 2, 3, 4, 5};
// 编译时确定大小的span
std::span<int, 5> sp1{arr}; // 编译器知道有5个元素
// 如果尝试创建不匹配的静态span,会产生编译错误
// std::span<int, 4> sp2{arr}; // 错误:尺寸不匹配
函数参数中的span
std::span
在函数参数中特别有用,它避免了不必要的拷贝,同时提供了更安全的接口:
// 过去的方式(危险且容易出错)
void process_data_old(int* data, size_t size) {
for (size_t i = 0; i < size; ++i) {
data[i] *= 2;
}
}
// 使用span的现代方式(安全且富有表现力)
void process_data_new(std::span<int> data) {
for (auto& elem : data) {
elem *= 2;
}
}
int main() {
std::vector<int> vec{1, 2, 3, 4, 5};
int arr[5] = {1, 2, 3, 4, 5};
// 旧方式(必须手动传递大小,容易出错)
process_data_old(vec.data(), vec.size());
process_data_old(arr, 5);
// 新方式(自动推导大小,统一接口)
process_data_new(vec);
process_data_new(arr);
}
模板与span结合的高级用法
当你需要编写能处理任意大小的固定数组的函数时,std::span
与模板结合使用非常强大:
template<typename T, std::size_t N>
void process_fixed_array(std::span<T, N> data) {
// 在编译时知道确切大小,可以启用更多优化
static_assert(N > 0, "Cannot process empty arrays");
// 可以安全地访问最后一个元素,因为编译器知道N > 0
T last = data[N-1];
// 处理数据...
}
int main() {
int arr5[5] = {1, 2, 3, 4, 5};
int arr10[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 编译器会为每种不同大小的数组生成不同的函数实例
process_fixed_array(std::span{arr5}); // 实例化 process_fixed_array<int, 5>
process_fixed_array(std::span{arr10}); // 实例化 process_fixed_array<int, 10>
}
常见错误与陷阱
使用std::span
时需要特别注意几个常见错误:
- 1. 引用临时对象
最危险的错误之一是创建引用临时对象的span
:
// 危险:getData()返回一个临时vector,span引用的内存很快会被释放
std::vector<int> getData() { return {1, 2, 3, 4, 5}; }
void dangerous() {
std::span<int> sp{getData()}; // 严重错误:引用了临时对象
// sp中的指针现在指向已释放的内存
std::cout << sp[0]; // 未定义行为
}
- 2. 忘记维护被引用对象的生命周期
std::span
是非拥有性视图,它不控制底层数据的生命周期:
std::span<int> create_span() {
std::vector<int> vec{1, 2, 3}; // 局部变量
return std::span{vec}; // 错误:返回引用局部变量的span
} // vec在这里被销毁
void use_span() {
auto sp = create_span(); // sp现在引用无效内存
std::cout << sp[0]; // 未定义行为
}
- 3. 忽略const修饰符
即使span
本身是const
的,它引用的元素可能不是const
的:
void process(const std::span<int> sp) {
sp[0] = 42; // 合法!虽然span是const的,但元素不是
}
// 如果想防止修改元素,应使用:
void process_readonly(std::span<const int> sp) {
// sp[0] = 42; // 错误:不能修改const元素
}
面试可能出现的问题
关于std::span
可能出现以下问题:
- • 问题:
std::span
与传统指针+长度方法相比有什么优势?
答案:std::span
提供类型安全,支持范围检查,简化接口,使代码意图更明确,支持范围for循环,提供丰富的成员函数,且在静态范围情况下能启用编译时优化。 - • 问题:
std::span
是按值传递还是按引用传递更好?
答案:std::span
推荐按值传递。它内部只包含一个指针和一个大小(对于动态范围),复制代价很小。按值传递能保持语义一致性,且避免引用的引用的复杂性。 - • 问题:
std::span<T>
和std::span<const T>
有什么区别?
答案:std::span<T>
允许修改被引用的元素,而std::span<const T>
只允许读取元素,不能修改。这类似于T*
和const T*
的区别。 - • 问题:如何安全地从函数返回
std::span
?
答案:返回std::span
时必须确保它引用的数据生命周期长于span
本身。通常应避免返回引用局部变量的span
,而应返回引用全局数据、静态数据或传入参数的子范围的span
。
总结
std::span
填补了C++中长期缺失的"视图"概念,为我们提供了一种既安全又高效的方式来处理内存区域,而不必担心所有权问题。这种非拥有性视图的概念已经在C++的其他部分(如string_view
)中证明了其价值,而std::span
将这一概念扩展到了任意类型的连续序列。
本文首发于【讳疾忌医-note】公众号,未经授权,不得转载。
(加入我的知识星球,免费获取账号,解锁所有文章。)
阅读剩余
版权声明:
作者:讳疾忌医-note
链接:https://www.1217zy.vip/archives/1093
文章版权归作者所有,未经允许请勿转载。
THE END