在C++20中,std::span被引入作为一个轻量级的、非拥有的数组视图。它在多种场景下极大地简化了代码,提高了可读性和安全性。本文将深入探讨std::span的核心特性、常见使用模式以及如何在已有项目中逐步迁移。
1. 何为 std::span
std::span 只保存了指向连续存储的指针和长度信息,它不拥有底层数据,也不负责内存管理。它的声明方式为:
template< class T, size_t Extent = dynamic_extent >
class span;
T:元素类型Extent:大小,若为dynamic_extent(默认)则大小在运行时确定;若为常量,则在编译期固定。
2. 基本使用
2.1 创建
std::vector <int> vec{1,2,3,4,5};
std::span <int> s1(vec); // 从容器创建
int arr[] = {10,20,30};
std::span <int> s2(arr); // 从数组创建
std::span <int> s3{vec.data(), 3}; // 指定长度
2.2 访问
for (auto val : s1) std::cout << val << ' ';
std::cout << "\n";
std::cout << s1[2] << '\n'; // 访问
s1[2] = 99; // 修改
2.3 子视图
auto sub = s1.subspan(1, 3); // 从索引1开始,长度3
auto tail = s1.last(2); // 最后2个元素
auto head = s1.first(2); // 前2个元素
3. 与容器的协同
3.1 作为参数
void process(std::span<const int> data) {
for (int v : data) {
// ...
}
}
process(vec); // 直接传递 vector
process(arr); // 直接传递数组
3.2 与算法配合
std::sort(s1.begin(), s1.end()); // 使用标准算法
3.3 与字符串
std::string str = "Hello, world!";
std::span <char> span_str(str.data(), str.size());
span_str[0] = 'h'; // 修改原字符串
4. 性能与安全
- 零成本:
std::span仅包含两个指针,编译器可以轻松优化为裸指针。 - 范围检查:
span::at提供边界检查,默认访问不检查。 - 不可变:使用
std::span<const T>可以强制只读访问,避免意外修改。
5. 常见误区
- 误认为拥有所有权
span并不管理内存,必须确保底层数据在使用期间有效。 - 对齐与布局
由于span不复制元素,使用时需保证底层容器连续布局(如std::vector、数组、std::array)。 - 不适用于链表
链表等非连续存储容器不能直接转换为span。
6. 逐步迁移示例
假设已有函数 void foo(int* data, size_t n),可改为:
void foo(std::span <int> data) {
// 现在可以使用更安全的接口
}
在调用点:
int arr[5] = {1,2,3,4,5};
foo(arr); // 自动匹配
std::vector <int> v = {1,2,3};
foo(v); // 也能直接传递
7. 进阶:span 的扩展
std::dynamic_extent:使用动态长度时的占位符。std::as_bytes与std::as_writable_bytes:将任意对象转换为字节视图。std::ranges::subrange:在C++23中与span兼容,提供更丰富的范围操作。
8. 结语
std::span 在C++20中以其简洁性与安全性为语言生态注入了新的活力。它不仅能让函数接口更具表达力,也能让代码在保持高性能的同时减少错误。无论是新项目还是维护已有代码,熟练掌握span都是提升代码质量的重要一步。