span - 容器视图¶
span<T> 是 C++20 引入的容器视图类,提供对连续序列的非拥有访问。我们需要在 C++17 环境下使用,所以自己实现了一份。核心思想很简单——只持有指针和长度,不管理数据生命周期——这使得 span 可以零拷贝地"切分"任何连续容器。
为什么需要 span¶
考虑一个函数需要接收整型数据的场景:
// 只能接受 vector
void process(const std::vector<int>& data);
// 只能接受 C 数组(但会退化成指针,丢失长度)
void process(int* data, size_t size);
第一种限制了调用方必须用 vector,第二种需要手动传长度而且容易出错。用 span 就没有这些问题:
// 可以接受任何容器
void process(cf::span<const int> data);
std::vector<int> vec = {1, 2, 3};
std::array<int, 3> arr = {1, 2, 3};
int c_arr[] = {1, 2, 3};
process(vec); // OK
process(arr); // OK
process(c_arr); // OK
构造方式¶
span 可以从任何连续容器构造,编译器会自动推导大小:
#include "base/span/span.h"
// 从 C 数组构造
int arr[] = {1, 2, 3, 4, 5};
cf::span<int> s1 = arr; // 自动推导长度
// 从 vector 构造
std::vector<int> vec = {1, 2, 3};
cf::span<int> s2 = vec;
// 从 array 构造
std::array<int, 4> arr2 = {1, 2, 3, 4};
cf::span<int> s3 = arr2;
// 手动指定指针和长度
cf::span<int> s4(vec.data(), vec.size());
元素访问¶
访问元素的方式和标准容器一样,支持 operator[]、front()、back():
cf::span<int> s = /* ... */;
int first = s[0]; // 下标访问
int first2 = s.front(); // 首元素
int last = s.back(); // 末元素
int* ptr = s.data(); // 底层指针
⚠️ operator[] 不做边界检查,越界访问是未定义行为。如果需要安全检查,标准库提供了 at() 方法,但我们的实现里为了性能省略了。
切片操作¶
span 最强大的功能是切片——可以轻松获取子视图而不拷贝数据:
cf::span<int> s = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 前三个元素
auto first_three = s.first(3); // {1, 2, 3}
// 后三个元素
auto last_three = s.last(3); // {8, 9, 10}
// 中间的切片
auto middle = s.subspan(2, 4); // {3, 4, 5, 6}
// 从位置 2 到末尾
auto tail = s.subspan(2); // {3, 4, 5, 6, 7, 8, 9, 10}
切片返回的新 span 仍指向原始数据,只是起始位置和长度不同。这意味着切片操作是 O(1) 的,没有任何拷贝开销。
函数参数¶
用 span 做函数参数是最常见的使用场景,特别是处理二进制数据或网络协议时:
// 写一个处理网络包的函数
void process_packet(cf::span<const uint8_t> packet) {
if (packet.size() < 4) return; // 包头至少 4 字节
auto header = packet.first(4);
auto payload = packet.subspan(4);
// 处理...
}
// 调用方可以传入任何缓冲区
std::vector<uint8_t> buffer = read_from_socket();
process_packet(buffer);
uint8_t stack_buf[256];
size_t received = recv(sock, stack_buf, 256, 0);
process_packet(cf::span<uint8_t>(stack_buf, received));
const 正确性¶
span<const T> 表示只读视图,span<T> 表示可写视图。选择正确的类型可以避免意外修改:
void read_only(cf::span<const int> data);
void read_write(cf::span<int> data);
const std::vector<int> vec = {1, 2, 3};
read_only(vec); // OK
read_write(vec); // 编译错误
生命周期陷阱¶
span 不拥有数据,只是一个"窗口"。如果底层数据被销毁,span 就会变成悬空引用:
cf::span<int> get_bad_span() {
std::vector<int> vec = {1, 2, 3};
return vec; // 警告:vec 会被销毁,返回的 span 悬空
}
// 正确的做法
cf::span<const int> get_good_span(const std::vector<int>& vec) {
return vec; // OK,调用方保证 vec 有效
}
这个坑在异步代码里特别容易出现——如果在一个线程里创建 span,另一个线程里使用,必须确保原始数据的生命周期足够长。