std::span¶
| 标准 | 难度 | 状态 | 分级 | 证据 | 审阅 | 最后校验 |
|---|---|---|---|---|---|---|
| C++20 | 3/5 | final | A | 3 条(manual 3) | 单审(zyuhao7) | 2026-10-24 |
编译器支持:GCC 10+ · Clang 7+ · MSVC 19.26+。版本查询:cppreference 编译器支持。
主张与证据¶
| 主张 | 支持证据 |
|---|---|
| 返回可写 span |
ev-mold-span-buffer-003 |
| 把重定位表表示为 span<const ElfRel |
ev-mold-span-relocations-002 |
| 用 span 与 subspan 把缓冲区切成固定大小的块 | ev-mold-span-split-001 |
本站证据页:ev-mold-span-buffer-003 · ev-mold-span-relocations-002 · ev-mold-span-split-001
解决什么问题¶
“一块连续内存 + 长度”是 C 风格接口最常见的参数组合,但两个参数可以彼此不一致:指针有效而长度错误,或长度正确而指针已失效。调用方无法从类型上看出函数是否拥有内存、是否允许写入。模板代码里还经常被迫为 std::vector、std::array、C 数组分别写重载。
std::span<T> 把指针与长度绑定成一个轻量视图。它不拥有数据,可隐式从数组、std::array、std::vector 构造,并保留元素类型与可写性。它解决的是接口表达问题,不是生命周期问题:视图存活多久仍由调用方保证。
基础用法¶
#include <array>
#include <iostream>
#include <span>
#include <vector>
int sum(std::span<const int> values) {
int total = 0;
for (int value : values) total += value;
return total;
}
int main() {
int raw[] = {1, 2, 3};
std::array<int, 2> arr{4, 5};
std::vector<int> vec{6, 7};
std::cout << sum(raw) << '\n';
std::cout << sum(arr) << '\n';
std::cout << sum(vec) << '\n';
}
完整示例见 examples/cpp20/span.cpp:
三种容器都隐式转成同一个视图类型;函数不需要模板,也不需要为每种容器写重载。
进阶用法¶
静态长度与动态长度。 std::span<int, 3> 在类型里固定长度,适合编译期已知大小的缓冲区;默认的 std::span<int> 使用 std::dynamic_extent,长度在运行期保存。静态长度版本可以避免长度字段,但构造时要求实际长度匹配。
span<const T> 与 const span<T> 不是一回事。 前者禁止通过视图修改元素;后者只是视图本身不可重新赋值,元素仍可写。接口想表达“只读”时用 span<const T>,并把它按值传递。
subspan 返回新的视图。 s.subspan(offset, count)、s.first(n)、s.last(n) 都不复制数据。运行期参数产生动态长度视图;编译期常量参数可以保留静态长度。越界是调用方责任,标准不检查。
没有边界检查。 operator[]、front、back 在越界或空视图上是未定义行为。C++26 加入 at() 才提供抛异常版本;在此之前,需要检查时自己写 if (i < s.size())。
生命周期不会延长。 std::span<const int> s = std::vector<int>{1, 2, 3}; 中临时 vector 在完整表达式结束后销毁,s 立即悬垂。不要从临时容器、临时数组或返回局部数组的函数构造长期存活的 span。把 span 作为函数参数是安全的,因为调用期间实参仍然存活。
按值传递。 span 通常只有指针与长度两个机器字,复制廉价;不要写 const std::span<T>&,那会多一层间接并阻止编译器优化。
与 std::string_view 的分工。 string_view 专门表示文本,带字符语义与字符串操作;span<const char> 只表示字节序列。处理文本用 string_view,处理二进制或泛型连续内存用 span。
多维数据用 std::mdspan。 C++23 的 mdspan 在 span 之上增加维度与步长;二维矩阵、图像缓冲区不要用嵌套 span 手工计算索引。
真实项目证据¶
| 项目 | 位置 | 用法 | 为什么典型 |
|---|---|---|---|
| rui314/mold | src/output-chunks.cc#L880-L892 |
用 span 与 subspan 把缓冲区切成固定大小的块 |
视图切片不复制数据,长度信息随视图传递 |
| rui314/mold | src/arch-x86-64.cc#L367-L374 |
把重定位表表示为 span<const ElfRel<E>> |
只读接口用 span<const T> 表达,调用方无需传长度 |
| rui314/mold | src/gdb-index.cc#L637-L642 |
返回可写的 span<u8> 指向压缩缓冲区 |
非拥有视图同时表达起始地址、长度与可写性 |
三条证据均为 manual 级:作者复核,尚未引入第二名审核者。mold 的构建配置声明 cxx_std_20。
陷阱与边界¶
- 悬垂是首要风险。
span不拥有数据;容器重新分配、局部数组销毁、临时对象结束都会让视图失效。 - 不要从临时容器构造长期视图。 函数返回
span指向局部变量是典型错误;返回vector或让调用方提供缓冲区。 - 越界不检查。
s[i]、s.front()、s.back()越界是未定义行为;C++26 之前没有at()。 span<const T>不能写,const span<T>可以写。 把只读意图写成后者会允许修改元素,破坏接口约定。- 静态长度不是运行时保证。
span<int, 3>的长度在类型里,但从动态视图构造时标准只要求前置条件;违规不会自动抛异常。 - 不要用
span表达所有权。 需要拥有数据时用vector、array或自定义容器;span只借看。 span不保证空终止。 传给 C API 前确认是否需要'\0';文本优先string_view,需要 C 字符串时显式复制。- 异步接口要特别小心。 把
span存进回调或线程池,必须保证底层内存在回调执行期间仍然存活。
库联动¶
- mold:链接器在重定位、输出分块与压缩缓冲区之间大量使用
span,把“指针 + 长度”收敛成一个参数,并在切片时不复制数据。 - fmt / spdlog:格式化库的范围支持可以接受连续容器;把
span传给fmt::join可以按元素输出。注意span<char>不会按字符串处理,仍按字符序列格式化。 - nlohmann/json:解析器接受字节容器,
std::as_bytes产生的span<const std::byte>可直接作为输入;生命周期由调用方保证。 - 与 ranges 联动:
span是contiguous_range、sized_range与borrowed_range,可以直接参与views::take、views::reverse等组合。把span存进视图后仍要注意底层数据寿命。
横向对比¶
| 方案 | 拥有数据 | 携带长度 | 元素类型安全 | 主要代价 |
|---|---|---|---|---|
std::span<T> |
否 | 是 | 是 | 生命周期由调用方保证 |
| 指针 + 长度 | 否 | 手动 | 弱 | 两个参数可能不一致 |
std::string_view |
否 | 是 | 仅字符 | 不能表达可变字节或泛型元素 |
gsl::span |
否 | 是 | 是 | 额外依赖;与标准版行为略有差异 |
std::vector<T> |
是 | 是 | 是 | 堆分配与所有权转移 |
std::array<T, N> |
是 | 编译期固定 | 是 | 长度固定,不能接受动态输入 |
跨标准演进:C++20 引入 std::span 与 std::as_bytes;C++23 增加 std::mdspan 处理多维视图;C++26 计划加入 span::at() 等边界检查接口。当前代码若需要异常式越界检查,应自行封装。