跳到主要内容

C++ span(C++20)

什么是 std::span?

std::span 是 C++20 标准库中引入的一个新容器适配器,它提供了对连续内存序列的视图,而不需要拷贝元素。简单来说,std::span 是一个非拥有型容器,它只引用其他地方已存在的内存,不会分配或管理内存。

备注

std::span 不是一个真正意义上的容器,而是一个"视图",它不拥有元素,只是引用它们。

为什么需要 std::span?

在 C++20 之前,我们常常需要传递数组、动态数组、std::vector 等连续容器给函数。这导致了几个问题:

  • 需要编写多个重载函数以接受不同类型的连续容器
  • 传递裸指针和长度时容易出错
  • 使用引用传递大型容器时,无法表明函数是否会修改容器内容

std::span 解决了这些问题,它提供了一种统一的方式来访问不同类型的连续内存序列,同时保持类型安全并避免内存拷贝。

std::span 的基本使用

头文件和命名空间

cpp
#include <span>
using namespace std;

创建 span

std::span 可以从多种连续容器创建:

cpp
#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):

cpp
#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 提供了创建子视图的方法,不需要复制元素:

cpp
#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 提供了多种访问元素的方式:

cpp
#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):

cpp
#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 可以编写接受任何连续容器的函数,而不需要编写多个重载版本:

cpp
#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. 无需复制的数据处理

在处理大量数据时,避免不必要的复制可以提高性能:

cpp
#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 可以避免常见的指针错误,比如缓冲区溢出:

cpp
#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 是一个轻量级包装器,通常只包含一个指针和一个大小。这意味着:

  1. 按值传递 std::span 是高效的
  2. 创建 std::span 不涉及内存分配
  3. 使用 std::span 不会产生运行时开销

然而,需要注意的是,std::span 不会延长被引用内存的生命周期。如果原始容器被销毁,span 将成为悬空引用。

注意

确保 std::span 引用的内存在 span 的整个生命周期内保持有效。使用悬空的 span 会导致未定义行为!

与其他容器的比较

特性std::spanstd::vector数组引用指针和长度
内存所有权
运行时大小
迭代器支持部分支持需手动实现
类型安全
子视图支持需拷贝需手动计算
开销极小较大极小

总结

std::span 是 C++20 引入的强大工具,它提供了连续内存序列的视图,而不需要拷贝元素。主要优点包括:

  1. 统一接口:一个函数可以接受多种类型的连续容器
  2. 避免复制:提高大数据处理的性能
  3. 安全性:比裸指针更安全的访问方式
  4. 灵活性:易于创建子视图和进行范围操作

std::span 适用于需要访问连续内存区域但不需要拥有内存的场景,例如函数参数、算法实现等。

练习

  1. 创建一个函数,使用 std::span 接受任何连续容器,并返回其中的最大值和最小值。
  2. 编写一个函数,接受一个整数数组的 span,并将所有偶数替换为 0。
  3. 实现一个函数,接受一个 span 并创建三个等大小的子 span(尽可能相等)。
  4. 编写一个使用 std::span 的矩阵类,它不拥有数据但可以提供矩阵操作。

进一步阅读

通过掌握 std::span,你将拥有一个强大的工具,可以编写更高效、更安全的代码,特别是在处理连续内存序列时。