C++ std::span:零拷贝视图,安全高效的数据访问
各位同学,大家好。今天我们来深入探讨C++20引入的一个非常重要的工具——std::span。 std::span提供了一种零拷贝的方式来访问和操作连续内存块,它可以作为C数组和STL容器的通用视图,极大地提高了代码的灵活性、安全性和效率。
1. std::span 简介
std::span本质上是一个非拥有(non-owning)的连续内存区域的视图。这意味着std::span本身不负责管理其指向的内存,它只是提供了一种访问和操作该内存的途径。 这带来了显著的优势:
- 零拷贝: 创建
std::span不会复制数据,只是创建一个指向现有数据的指针和一个长度。 - 类型安全:
std::span知道它所指向的数据的类型和大小,从而可以在编译时进行类型检查,防止越界访问等错误。 - 通用性:
std::span可以用于C数组、std::vector、std::array等多种数据结构,提供一致的访问接口。 - 性能: 由于零拷贝和类型安全,
std::span通常能提供与直接使用指针相当的性能,同时避免了指针操作带来的风险。
2. std::span 的基本用法
std::span定义在 <span> 头文件中。它的模板定义如下:
template< class T, std::size_t Extent = std::dynamic_extent > class span;
T:span引用的元素的类型。Extent:span的长度。可以是编译时常量(静态extent)或运行时常量(动态extent)。
2.1 构造 std::span
std::span可以通过多种方式构造:
-
从C数组构造:
int arr[] = {1, 2, 3, 4, 5}; std::span<int> span_arr(arr, 5); // 动态extent std::span<int, 5> span_arr_static(arr); // 静态extent for (int i = 0; i < span_arr.size(); ++i) { std::cout << span_arr[i] << " "; } std::cout << std::endl; -
从
std::vector构造:std::vector<int> vec = {6, 7, 8, 9, 10}; std::span<int> span_vec(vec); // 动态extent for (int i = 0; i < span_vec.size(); ++i) { std::cout << span_vec[i] << " "; } std::cout << std::endl; -
从
std::array构造:std::array<int, 5> arr_std = {11, 12, 13, 14, 15}; std::span<int, 5> span_arr_std(arr_std); // 静态extent //std::span<int> span_arr_std(arr_std); // This is also valid for (int i = 0; i < span_arr_std.size(); ++i) { std::cout << span_arr_std[i] << " "; } std::cout << std::endl; -
从指针和长度构造:
int* data = new int[5]{16,17,18,19,20}; std::span<int> span_ptr(data, 5); // 动态extent for (int i = 0; i < span_ptr.size(); ++i) { std::cout << span_ptr[i] << " "; } std::cout << std::endl; delete[] data; // 重要:记得释放内存
2.2 std::span 的成员函数
std::span提供了一系列成员函数用于访问和操作数据:
| 函数 | 描述 |
|---|---|
size() |
返回span的长度。 |
empty() |
如果span为空,则返回true,否则返回false。 |
data() |
返回指向span所指向内存的指针。 |
front() |
返回span的第一个元素的引用。 |
back() |
返回span的最后一个元素的引用。 |
operator[] |
访问span中指定索引的元素。 |
subspan() |
创建一个新的span,它是原始span的一个子区域。 |
first() |
创建一个新的span,包含原始span的前N个元素。 |
last() |
创建一个新的span,包含原始span的后N个元素。 |
示例:使用 subspan()
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::span<int> full_span(data);
// 创建一个包含从索引2开始的3个元素的子span
std::span<int> sub_span = full_span.subspan(2, 3);
for (int i = 0; i < sub_span.size(); ++i) {
std::cout << sub_span[i] << " "; // 输出: 3 4 5
}
std::cout << std::endl;
// 创建一个包含从索引2到末尾的子span
std::span<int> sub_span_to_end = full_span.subspan(2);
for (int i = 0; i < sub_span_to_end.size(); ++i) {
std::cout << sub_span_to_end[i] << " "; // 输出: 3 4 5 6 7 8 9 10
}
std::cout << std::endl;
// 创建一个包含前3个元素的子span
std::span<int> first_span = full_span.first(3);
for (int i = 0; i < first_span.size(); ++i) {
std::cout << first_span[i] << " "; // 输出: 1 2 3
}
std::cout << std::endl;
// 创建一个包含后3个元素的子span
std::span<int> last_span = full_span.last(3);
for (int i = 0; i < last_span.size(); ++i) {
std::cout << last_span[i] << " "; // 输出: 8 9 10
}
std::cout << std::endl;
3. 静态Extent vs 动态Extent
std::span的长度可以是静态的(编译时已知)或动态的(运行时已知)。
-
静态Extent:
- 长度在编译时确定。
- 使用
std::span<T, N>声明,其中N是一个编译时常量。 - 优点:可以进行更多的编译时优化,例如,编译器可以内联对
size()的调用。 - 缺点:灵活性较差,只能用于长度固定的数据。
-
动态Extent:
- 长度在运行时确定。
- 使用
std::span<T>或std::span<T, std::dynamic_extent>声明。 - 优点:灵活性强,可以用于长度可变的数据。
- 缺点:编译时优化可能较少。
示例:静态Extent vs 动态Extent
#include <iostream>
#include <array>
#include <span>
void print_static_span(std::span<int, 5> span) {
std::cout << "Static span: ";
for (int i = 0; i < span.size(); ++i) {
std::cout << span[i] << " ";
}
std::cout << std::endl;
}
void print_dynamic_span(std::span<int> span) {
std::cout << "Dynamic span: ";
for (int i = 0; i < span.size(); ++i) {
std::cout << span[i] << " ";
}
std::cout << std::endl;
}
int main() {
std::array<int, 5> arr = {1, 2, 3, 4, 5};
std::vector<int> vec = {6, 7, 8, 9, 10, 11};
std::span<int, 5> static_span(arr); // 静态Extent
std::span<int> dynamic_span_arr(arr); // 动态Extent
std::span<int> dynamic_span_vec(vec); // 动态Extent
print_static_span(static_span);
print_dynamic_span(dynamic_span_arr);
print_dynamic_span(dynamic_span_vec);
return 0;
}
4. std::span 与 const
std::span可以指向可修改的或只读的数据。
std::span<int>:指向可修改的int数据的span。std::span<const int>:指向只读的int数据的span。
示例:std::span 与 const
#include <iostream>
#include <vector>
#include <span>
void modify_span(std::span<int> span) {
if (!span.empty()) {
span[0] = 100; // 可以修改span指向的数据
}
}
void print_span(std::span<const int> span) {
std::cout << "Span: ";
for (int i = 0; i < span.size(); ++i) {
std::cout << span[i] << " ";
}
std::cout << std::endl;
}
int main() {
std::vector<int> data = {1, 2, 3, 4, 5};
std::span<int> mutable_span(data);
std::span<const int> const_span(data);
print_span(const_span); // 输出: Span: 1 2 3 4 5
modify_span(mutable_span);
print_span(const_span); // 输出: Span: 100 2 3 4 5 (数据被修改了)
// 无法通过 const_span 修改数据
// const_span[0] = 200; // 编译错误!
return 0;
}
5. std::span 的优势
- 安全性:
std::span提供了类型安全和边界检查,减少了指针操作带来的风险。使用std::span可以避免悬挂指针和缓冲区溢出等问题。 - 效率:
std::span是零拷贝的,避免了不必要的数据复制,提高了性能。 - 通用性:
std::span可以用于多种数据结构,提供一致的访问接口,提高了代码的复用性。 - 代码可读性: 使用
std::span可以更清晰地表达代码的意图,提高代码的可读性和可维护性。
6. std::span 的应用场景
-
函数参数传递: 使用
std::span作为函数参数,可以接受 C 数组、std::vector、std::array等多种类型的数据,提高了函数的通用性。void process_data(std::span<int> data) { // 处理数据 for (int i = 0; i < data.size(); ++i) { std::cout << data[i] << " "; } std::cout << std::endl; } int main() { int arr[] = {1, 2, 3}; std::vector<int> vec = {4, 5, 6}; process_data(arr); // 可以传递 C 数组 process_data(vec); // 可以传递 std::vector return 0; } -
算法实现: 在算法实现中使用
std::span可以提高算法的通用性,使其可以处理多种类型的数据。int sum(std::span<const int> data) { int result = 0; for (int value : data) { result += value; } return result; } int main() { int arr[] = {1, 2, 3, 4, 5}; std::vector<int> vec = {6, 7, 8, 9, 10}; std::cout << "Sum of array: " << sum(arr) << std::endl; std::cout << "Sum of vector: " << sum(vec) << std::endl; return 0; } -
库的设计: 在库的设计中使用
std::span可以提供更灵活和安全的接口。
7. std::span 和 std::string_view 的比较
std::string_view 和 std::span 都是 C++17 引入的非拥有视图,但它们的应用场景不同:
std::string_view:专门用于字符串(char或wchar_t数组)的只读视图。它假设数据是 null 结尾的。std::span:用于任意连续内存区域的视图,不要求 null 结尾。它可以是可修改的或只读的。
8. 注意事项
std::span不拥有它所指向的内存,因此在使用std::span时,必须确保它所指向的内存的生命周期长于std::span本身的生命周期。否则,会导致悬挂指针的问题。- 在使用
std::span时,要特别注意边界检查,避免越界访问。虽然std::span提供了一些边界检查,但仍然需要自己进行额外的验证。 - 当使用从指针构造
std::span时,尤其要保证指针的有效性, 并在不再使用指针指向的内存后,释放内存。
9. 总结:std::span 提升代码质量
std::span 是一个强大的工具,它提供了一种安全、高效和通用的方式来访问和操作连续内存块。 通过学习和使用std::span,我们可以编写出更健壮、更灵活和更易于维护的C++代码。 在函数参数传递和算法实现等场景中,std::span 都能够发挥重要作用,提升代码质量。
更多IT精英技术系列讲座,到智猿学院