深入学习 std::span
头文件:
<span>命名空间:std编译器要求:C++20 起
一、设计动机:统一连续内存的访问接口
1.1 C++ 中连续内存的 N 种传参方式
在没有 span 之前,传递"一段连续内存"的方式五花八门:
| |
核心问题: 没有一种统一的、类型安全的方式说"我只需要一段连续内存的只读/可写视图"。
1.2 span 的解法
| |
一句话总结:span 是连续内存的"通用视图"——不拥有数据、不分配内存、只是指针+长度的薄封装。
二、span 的本质:指针 + 长度
2.1 内部结构
| |
| |
2.2 与 string_view 的对比
| 维度 | std::span<T> | std::string_view |
|---|---|---|
| 数据类型 | 任意类型 T | 仅 char |
| 可变性 | span<T> 可修改;span<const T> 只读 | 始终只读 |
| 用途 | 通用连续内存视图 | 字符串只读视图 |
| 空终止 | 不要求 | 不要求(但底层可能是) |
| sizeof | 16 bytes(动态)/ 8 bytes(静态) | 16 bytes |
span 是 string_view 的泛化版本——string_view ≈ span<const char>(语义上)。
三、静态 Extent vs 动态 Extent
3.1 动态 Extent(默认)
| |
3.2 静态 Extent
| |
3.3 选择建议
| 场景 | 选择 | 理由 |
|---|---|---|
| 通用接口、大小运行时确定 | 动态 extent | 灵活性 |
| 固定大小的协议字段 | 静态 extent | 编译期检查 + 优化 |
| 接受任意子区间 | 动态 extent | 不限制调用者 |
| 嵌入式/实时系统 | 静态 extent | 零运行时开销 |
四、核心 API
4.1 构造
| |
4.2 子视图操作
| |
4.3 元素访问
| |
4.4 类型转换:as_bytes / as_writable_bytes
| |
五、生命周期——span 最重要的注意事项
5.1 span 不拥有数据
| |
5.2 经验法则
- span 主要用作函数参数——调用者拥有数据,函数内使用 span 访问
- 不要把 span 存入类成员——除非能保证数据生命周期更长
- 不要返回指向局部变量的 span——与返回指针/引用同理
- span 生存期必须 ≤ 底层数据生存期
六、实战场景
6.1 网络包零拷贝解析
| |
6.2 安全的缓冲区传递
| |
6.3 多维数据的切片
| |
七、span vs 其他传参方式对比
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
span<T> | 通用连续内存视图 | 统一接口、零拷贝 | 不拥有数据,需注意生命周期 |
const vector<T>& | 只接受 vector | 明确所有权 | 排斥 array/C数组 |
T*, size_t | C 互操作 | 兼容 C | 类型不安全,易传错长度 |
string_view | 字符串只读 | 零拷贝 | 仅限字符类型 |
模板 Container& | 需要完整容器功能 | 可调用 push_back 等 | 编译膨胀 |
经验法则: 如果函数只需要"读/写一段连续内存",用 span。
八、最佳实践总结
- 函数参数用 span 替代
const vector<T>&——接受更多类型的调用者 - span 主要用作参数和局部变量——不要存入类成员(除非能管理生命周期)
- 只读访问用
span<const T>——传达意图,防止意外修改 - 已知大小用静态 extent——编译期检查 + 更好的优化
- 网络/IO 层优先用
span<const std::byte>——类型安全的字节视图 - 子区间操作用 first/last/subspan——零拷贝切片
- 不要对可能 realloc 的容器长期持有 span——push_back 后 span 可能悬垂
- 与 C API 互操作用
.data()+.size()——span 到 C 接口的桥梁