yuqi-zheng

Replace const vector& with std::span: Zero-Cost Generality


A function that takes const std::vector<T>& works correctly but is unnecessarily restrictive. The caller must own a std::vector — even when the data lives in a C array, std::array, a mapped memory region, or a slice of an existing container. std::span (C++20) or absl::Span accepts all of these with zero overhead.

This is Abseil C++ Tip #93. The benchmarks below confirm it.


The Problem

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

Callers with a std::array or a C array are forced to construct a temporary std::vector, copying the data for no reason:

std::array<int, 4> a = {1, 2, 3, 4};
ProcessData({a.begin(), a.end()});  // allocates and copies

The function signature rejects valid inputs and forces unnecessary work.


The Fix

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

Now all of these work without allocation:

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

ProcessData(v);      // OK — binds directly
ProcessData(a);      // OK — binds directly
ProcessData(c_arr);  // OK — binds directly
ProcessData(v.subspan(100));  // OK — subrange, no copy

Benchmarks

All benchmarks call a function that sums the input elements (to prevent dead-code elimination). Four scenarios were tested:

ScenarioDescription
Big Vector1000-element std::vector passed directly
Small LiteralBrace-initialized {1, 2, 3, ...} call
C Arrayint arr[100] passed as input
SubrangeLast 500 elements of a 1000-element vector

Results

Scenarioconst vector&std::spanabsl::SpanSpeedup
Big Vector189 ns189 ns189 ns
Small Literal13.3 ns0.28 ns0.28 ns~48x
C Array50.6 ns18.3 ns18.3 ns~2.8x
Subrange148 ns95.9 ns95.9 ns~1.5x

When the caller already holds a std::vector, there is no difference — std::span is a zero-cost view. The gains appear when the caller holds anything else.

The “Small Literal” case is the most extreme: const vector& forces a heap allocation and copy for a 4-element array. std::span just records a pointer and a length.


When span Is Not Appropriate

Ownership transfer. If the function needs to store or modify the data beyond the call, it should take the container by value or by pointer, not by view.

Lifetime sensitivity. A span does not own its data. The caller must ensure the underlying storage outlives the span. This is the same contract as string_view — familiar territory for C++17 codebases.

Internal private APIs. A private helper that is only ever called with std::vector can keep const vector& for clarity. The main benefit is at public API boundaries.


Recommendation

For any read-only function parameter that currently takes const std::vector<T>&:

  1. If you are on C++20, replace with std::span<const T>.
  2. If you are on C++17 with Abseil, replace with absl::Span<const T>.
  3. If the function needs ownership, keep the vector (or take by value).

This is a mechanical change that improves generality and, in non-trivial calling patterns, performance.