C++ span(C++20)
什么是 std::span?
std::span
是 C++20 标准库中引入的一个新容器适配器,它提供了对连续内存序列的视图,而不需要拷贝元素。简单来说,std::span
是一个非拥有型容器,它只引用其他地方已存在的内存,不会分配或管理内存。
std::span
不是一个真正意义上的容器,而是一个"视图",它不拥有元素,只是引用它们。
为什么需要 std::span?
在 C++20 之前,我们常常需要传递数组、动态数组、std::vector
等连续容器给函数。这导致了几个问题:
- 需要编写多个重载函数以接受不同类型的连续容器
- 传递裸指针和长度时容易出错
- 使用引用传递大型容器时,无法表明函数是否会修改容器内容
std::span
解决了这些问题,它提供了一种统一的方式来访问不同类型的连续内存序列,同时保持类型安全并避免内存拷贝。
std::span 的基本使用
头文件和命名空间
#include <span>
using namespace std;
创建 span
std::span
可以从多种连续容器创建:
#include <iostream>
#include <span>
#include <vector>
#include <array>
int main() {
// 从C风格数组创建
int arr[] = {1, 2, 3, 4, 5};
std::span<int> sp1(arr);
// 从std::vector创建
std::vector<int> vec = {10, 20, 30, 40, 50};
std::span<int> sp2(vec);
// 从std::array创建
std::array<int, 3> arr2 = {100, 200, 300};
std::span<int> sp3(arr2);
// 输出所有span的内容
std::cout << "span from array: ";
for (auto i : sp1) std::cout << i << " ";
std::cout << "\n";
std::cout << "span from vector: ";
for (auto i : sp2) std::cout << i << " ";
std::cout << "\n";
std::cout << "span from std::array: ";
for (auto i : sp3) std::cout << i << " ";
std::cout << "\n";
return 0;
}
输出:
span from array: 1 2 3 4 5
span from vector: 10 20 30 40 50
span from std::array: 100 200 300
指定 span 大小
std::span
可以在编译时固定大小(静态span),也可以在运行时确定大小(动态span):
#include <iostream>
#include <span>
int main() {
int arr[] = {1, 2, 3, 4, 5};
// 动态大小的span
std::span<int> dynamic_span(arr);
std::cout << "Dynamic span size: " << dynamic_span.size() << "\n";
// 静态大小的span (编译时确定)
std::span<int, 5> static_span(arr);
std::cout << "Static span size: " << static_span.size() << "\n";
// 通过构造函数参数指定范围
std::span<int> partial_span(arr, 3); // 只使用前3个元素
std::cout << "Partial span: ";
for (auto i : partial_span) std::cout << i << " ";
std::cout << "\n";
return 0;
}
输出:
Dynamic span size: 5
Static span size: 5
Partial span: 1 2 3
span 的主要特性和操作
1. 子视图操作
std::span
提供了创建子视图的方法,不需要复制元素:
#include <iostream>
#include <span>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::span<int> sp(vec);
// 获取前5个元素
std::span<int> first_half = sp.first(5);
std::cout << "First 5 elements: ";
for (auto i : first_half) std::cout << i << " ";
std::cout << "\n";
// 获取后5个元素
std::span<int> second_half = sp.last(5);
std::cout << "Last 5 elements: ";
for (auto i : second_half) std::cout << i << " ";
std::cout << "\n";
// 获取中间部分 (从索引2开始的4个元素)
std::span<int> middle = sp.subspan(2, 4);
std::cout << "Middle 4 elements (starting from index 2): ";
for (auto i : middle) std::cout << i << " ";
std::cout << "\n";
return 0;
}
输出:
First 5 elements: 1 2 3 4 5
Last 5 elements: 6 7 8 9 10
Middle 4 elements (starting from index 2): 3 4 5 6
2. 访问元素
std::span
提供了多种访问元素的方式:
#include <iostream>
#include <span>
int main() {
int arr[] = {10, 20, 30, 40, 50};
std::span<int> sp(arr);
// 使用[]运算符
std::cout << "First element: " << sp[0] << "\n";
// 使用front()和back()
std::cout << "Front: " << sp.front() << "\n";
std::cout << "Back: " << sp.back() << "\n";
// 使用data()获取指针
int* ptr = sp.data();
std::cout << "First element via pointer: " << *ptr << "\n";
// 范围for循环
std::cout << "All elements: ";
for (auto i : sp) std::cout << i << " ";
std::cout << "\n";
return 0;
}
输出:
First element: 10
Front: 10
Back: 50
First element via pointer: 10
All elements: 10 20 30 40 50
3. 修改元素
std::span
允许修改元素(如果元素类型不是 const):
#include <iostream>
#include <span>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::span<int> sp(vec);
// 修改元素
for (size_t i = 0; i < sp.size(); ++i) {
sp[i] *= 10;
}
// 查看修改后的结果
std::cout << "Modified vector via span: ";
for (auto i : vec) std::cout << i << " ";
std::cout << "\n";
// 使用const span防止修改
std::span<const int> const_sp(vec);
// const_sp[0] = 100; // 编译错误,不能修改const span的元素
return 0;
}
输出:
Modified vector via span: 10 20 30 40 50
std::span 的实际应用场景
1. 函数参数统一
使用 std::span
可以编写接受任何连续容器的函数,而不需要编写多个重载版本:
#include <iostream>
#include <span>
#include <vector>
#include <array>
// 一个计算总和的函数,可以接受任何连续容器
int sum(std::span<const int> numbers) {
int result = 0;
for (int num : numbers) {
result += num;
}
return result;
}
int main() {
// 使用不同的容器类型
int arr[] = {1, 2, 3, 4, 5};
std::vector<int> vec = {10, 20, 30};
std::array<int, 2> std_arr = {100, 200};
// 同一个函数可以处理所有类型
std::cout << "Sum of array: " << sum(arr) << "\n";
std::cout << "Sum of vector: " << sum(vec) << "\n";
std::cout << "Sum of std::array: " << sum(std_arr) << "\n";
return 0;
}
输出:
Sum of array: 15
Sum of vector: 60
Sum of std::array: 300
2. 无需复制的数据处理
在处理大量数据时,避免不必要的复制可以提高性能:
#include <iostream>
#include <span>
#include <vector>
#include <algorithm>
// 处理数据的函数
void processData(std::span<int> data) {
// 对数据进行排序
std::sort(data.begin(), data.end());
// 其他处理...
}
int main() {
std::vector<int> largeData = {5, 2, 8, 1, 9, 3, 7, 4, 6};
std::cout << "Original data: ";
for (int i : largeData) std::cout << i << " ";
std::cout << "\n";
// 处理数据,不需要复制
processData(largeData);
std::cout << "Processed data: ";
for (int i : largeData) std::cout << i << " ";
std::cout << "\n";
return 0;
}
输出:
Original data: 5 2 8 1 9 3 7 4 6
Processed data: 1 2 3 4 5 6 7 8 9
3. 增强安全性
std::span
可以避免常见的指针错误,比如缓冲区溢出:
#include <iostream>
#include <span>
#include <vector>
// 使用span的安全函数
void safeAccess(std::span<int> data, size_t index) {
if (index < data.size()) {
std::cout << "Safe access: value at index " << index << " is " << data[index] << "\n";
} else {
std::cout << "Safe access: index " << index << " is out of bounds\n";
}
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::span<int> sp(vec);
// 安全访问
safeAccess(sp, 2); // 有效索引
safeAccess(sp, 10); // 无效索引
return 0;
}
输出:
Safe access: value at index 2 is 3
Safe access: index 10 is out of bounds
性能考虑
std::span
是一个轻量级包装器,通常只包含一个指针和一个大小。这意味着:
- 按值传递
std::span
是高效的 - 创建
std::span
不涉及内存分配 - 使用
std::span
不会产生运行时开销
然而,需要注意的是,std::span
不会延长被引用内存的生命周期。如果原始容器被销毁,span 将成为悬空引用。
确保 std::span
引用的内存在 span 的整个生命周期内保持有效。使用悬空的 span 会导致未定义行为!
与其他容器的比较
特性 | std::span | std::vector | 数组引用 | 指针和长度 |
---|---|---|---|---|
内存所有权 | ❌ | ✅ | ❌ | ❌ |
运行时大小 | ✅ | ✅ | ❌ | ✅ |
迭代器支持 | ✅ | ✅ | 部分支持 | 需手动实现 |
类型安全 | ✅ | ✅ | ✅ | ❌ |
子视图支持 | ✅ | 需拷贝 | ❌ | 需手动计算 |
开销 | 极小 | 较大 | 无 | 极小 |
总结
std::span
是 C++20 引入的强大工具,它提供了连续内存序列的视图,而不需要拷贝元素。主要优点包括:
- 统一接口:一个函数可以接受多种类型的连续容器
- 避免复制:提高大数据处理的性能
- 安全性:比裸指针更安全的访问方式
- 灵活性:易于创建子视图和进行范围操作
std::span
适用于需要访问连续内存区域但不需要拥有内存的场景,例如函数参数、算法实现等。
练习
- 创建一个函数,使用
std::span
接受任何连续容器,并返回其中的最大值和最小值。 - 编写一个函数,接受一个整数数组的 span,并将所有偶数替换为 0。
- 实现一个函数,接受一个 span 并创建三个等大小的子 span(尽可能相等)。
- 编写一个使用
std::span
的矩阵类,它不拥有数据但可以提供矩阵操作。
进一步阅读
- C++ 参考文档中的 std::span
- C++20 标准的相关章节
- 《Effective Modern C++》中关于视图和所有权的讨论
通过掌握 std::span
,你将拥有一个强大的工具,可以编写更高效、更安全的代码,特别是在处理连续内存序列时。