之前已经介绍了std::string_view,std::span可以说是它的拓展版本
C++笔记:std::string_view_std::stringview-CSDN博客
std::span 是"对一段连续内存的非拥有视图(view)"
零拷贝、零分配,只描述"指针 + 长度"
一句话直觉理解
std::span<T> ≈ T* + size
但比裸指针 安全、语义清晰、可组合。
类声明
cpp
#include<span>
template<
class T,
std::size_t Extent = std::dynamic_extent//inline constexpr size_t dynamic_extent = static_cast<size_t>(-1);
> class span;
对象构造方式
我们可以从各种东西构造一个span,由于可以传进来的形式过多,不一一介绍
cpp
int a[5] = {1,2,3,4,5};
std::vector<int> v = {1,2,3};
std::array<int,3> arr = {1,2,3};
std::span<int> s1(a); // C array
std::span<int> s2(v); // vector
std::span<int> s3(arr); // std::array
span构造的本质限制:只能构造自"连续内存"
你可以传数组,指针,容器类,容器迭代器,但是必须要保证内存连续,所以
cpp
std::list<int>
std::set<int>
std::deque<int>
std::map<...>
这一类容器及其迭代器,是不能用来构造span的。
span<T> vs span<T, N>
span<T>:动态大小(最常用)
span<T, N>:静态大小(更严格)
| 特性 | 说明 |
|---|---|
| 大小 | 编译期常量 |
| 类型 | span<int,4> ≠ span<int,5> |
| 检查 | 编译期更安全 |
| 开销 | 一样(size 不存) |
cpp
int a[4];
std::span<int,4> s(a); // ✔
std::vector<int> v(4);
std::span<int,4> s(v); // ❌(vector size 非 constexpr)
像容器一样使用
常见的使用方式就不过多介绍了,接口和之前设计的基本一样
cpp
for (int& x : s1) {
x *= 2;
}
std::cout << s1[0]; // unchecked
std::cout << s1.at(0); // bounds-checked
子视图
|-------|-------------------|
| first | 获得由序列首 N 个元素组成的子段 |
| last | 获得由序列末 N 个元素组成的子段 |
cpp
// 编译期长度
template< std::size_t Count >
constexpr std::span<element_type, Count> first() const;
// 运行期长度
constexpr std::span<element_type, std::dynamic_extent> first( std::size_t Count ) const;
// 编译期长度
template< std::size_t Count >
constexpr std::span<element_type, Count> last() const;
// 运行期长度
constexpr std::span<element_type, std::dynamic_extent> last( std::size_t Count ) const;
注意,要是长度不合法,结果是UB的,并不会做边界检查
subspan
cpp
// 编译期 offset + count
template< std::size_t Offset,
std::size_t Count = std::dynamic_extent >
constexpr auto subspan() const;
// 运行期 offset + count
constexpr std::span<element_type, std::dynamic_extent>
subspan( std::size_t Offset,
std::size_t Count = std::dynamic_extent ) const;
cpp
auto mid = s.subspan(2, 4);//运行期版本
auto rest = s.subspan(2);//忽略 count(取到结尾)
//返回类型:std::span<T, 8>
auto payload = s.subspan<4, 8>();//编译期版本
转换 span 为对其底层字节的视图
std::as_bytes, std::as_writable_bytes这两把"任意类型的 span"视为"字节序列 span"
-
std::as_bytes👉 只读视图 (
const std::byte) -
std::as_writable_bytes👉 可写视图 (
std::byte)
⚠️ 不拷贝数据,只是 reinterpret 视图
cpp
#include <span>
// 只读
template <class T, size_t Extent>
span<const std::byte,
Extent == dynamic_extent
? dynamic_extent
: Extent * sizeof(T)>
as_bytes(span<T, Extent> s) noexcept;
// 可写
template <class T, size_t Extent>
span<std::byte,
Extent == dynamic_extent
? dynamic_extent
: Extent * sizeof(T)>
as_writable_bytes(span<T, Extent> s) noexcept;
返回 span 的元素类型变了,长度也变了。类型变成了std::byte字节类型,长度变成了字节数
比如你要读写字节流,就可能需要这两个函数
cpp
struct Header {
uint32_t len;
uint16_t type;
};
Header h{100, 2};
std::span<Header> sh(&h, 1);
auto bytes = std::as_bytes(sh);
// bytes 是 span<const std::byte>
send(fd, bytes.data(), bytes.size());
有什么应用场景?
作为标准库引入的新类,并且原理并不复杂,肯定是有相当的场景需要用span
场景 1️⃣:公共 API 的"输入参数"
cpp
void process(const float* data, size_t n);//以前
void process(std::span<const float> data);//有了 span(标准推荐写法)
好处(不是语法糖)
-
✔️ 数据 + 长度绑定
-
✔️ 明确连续内存
-
✔️ 明确只读
-
✔️ 可接:
-
vector -
array -
C 数组
-
子 span
-
场景 2️⃣:协议 / 二进制解析
cpp
void parse(std::span<const std::byte> buf) {
auto header = buf.first<8>();
auto body = buf.subspan(8);
}
这样解析非常快速
没有 span 就只能写:
cpp
const uint8_t* p = buf + 8;
size_t len = total - 8;
场景 3️⃣:零拷贝切片(first / last / subspan)
span 的切片是 视图语义:
cpp
auto body = buf.subspan(16, payload_len);
-
❌ 不分配
-
❌ 不拷贝
-
✔️ 明确边界
场景 4️⃣:替代"const vector&"作为参数
错误但常见的接口设计
cpp
void foo(const std::vector<int>& v);
问题:
-
❌ 只能传 vector
-
❌ 不能传 array / span / 子区间
-
❌ 不表达"是否需要 owning"
正确的现代接口
cpp
void foo(std::span<const int> v);
👉 容器解耦
场景 5️⃣:字节级操作(as_bytes)
span + bytes = 标准化 raw buffer
cpp
hash(std::as_bytes(span(data)));
这块我倒是有点体会,以前必须要把data通过reinterpret_cast成uint_8或者char这种字节数组。现在可以直接通过标准库转化成字节。
什么时候"不该"用 span?
❌ 不要用 span 表示"拥有数据"
❌ 不要存 span 作为长期成员
❌ 不要跨线程长期保存