yuqi-zheng

用 std::span 替代 const vector&:零成本的通用性


一个接受 const std::vector<T>& 的函数虽然能正确工作,但不必要地限制了调用者。调用者必须拥有一个 std::vector——即使数据存在 C 数组、std::array、内存映射区域或现有容器的切片中。std::span(C++20)或 absl::Span 可以零开销地接受所有这些类型。

这是 Abseil C++ Tip #93。下面的基准测试证实了这一点。


问题所在

void ProcessData(const std::vector<int>& data);

持有 std::array 或 C 数组的调用者被迫构造临时的 std::vector,毫无理由地复制数据:

std::array<int, 4> a = {1, 2, 3, 4};
ProcessData({a.begin(), a.end()});  // 分配并复制

这个函数签名拒绝了合法的输入,并强制执行了不必要的工作。


解决方案

void ProcessData(std::span<const int> data);

现在所有这些调用都能无需分配地工作:

std::vector<int> v = GetIds();
std::array<int, 3> a = {1, 2, 3};
int c_arr[] = {4, 5, 6};

ProcessData(v);      // OK — 直接绑定
ProcessData(a);      // OK — 直接绑定
ProcessData(c_arr);  // OK — 直接绑定
ProcessData(v.subspan(100));  // OK — 子范围,无需复制

基准测试

所有基准测试调用一个对输入元素求和的函数(防止编译器消除死代码)。测试了四种场景:

场景描述
大 Vector直接传入 1000 元素的 std::vector
小字面量用花括号初始化 {1, 2, 3, ...} 调用
C 数组int arr[100] 作为输入
子范围1000 元素 vector 的后 500 个元素

结果

场景const vector&std::spanabsl::Span加速比
大 Vector189 ns189 ns189 ns
小字面量13.3 ns0.28 ns0.28 ns~48 倍
C 数组50.6 ns18.3 ns18.3 ns~2.8 倍
子范围148 ns95.9 ns95.9 ns~1.5 倍

当调用者已经持有 std::vector 时,没有差异——std::span 是零开销视图。当调用者持有其他类型的容器时,优势就显现了。

“小字面量” 场景最为极端:const vector& 强制对一个 4 元素数组进行堆分配和复制。std::span 只是记录一个指针和一个长度。


什么时候不适合用 span

需要所有权转移。 如果函数需要存储或修改超出调用生命周期的数据,应该按值或按指针接收容器,而不是视图。

生命周期敏感。 span 不拥有数据。调用者必须确保底层数据的生命周期超过 span 的使用期。这与 string_view 的契约相同——对 C++17 代码库来说是熟悉的领域。

内部私有 API。 一个只被 std::vector 调用的私有辅助函数可以保留 const vector& 以保持清晰。主要收益在公共 API 边界。


建议

对于任何当前接受 const std::vector<T>& 的只读函数参数:

  1. 如果使用 C++20,替换为 std::span<const T>
  2. 如果使用 C++17 并依赖 Abseil,替换为 absl::Span<const T>
  3. 如果函数需要所有权,保留 vector(或按值接收)。

这是一个机械性的改动,能提升通用性,并在非平凡的调用模式中提升性能。