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:
| Scenario | Description |
|---|---|
| Big Vector | 1000-element std::vector passed directly |
| Small Literal | Brace-initialized {1, 2, 3, ...} call |
| C Array | int arr[100] passed as input |
| Subrange | Last 500 elements of a 1000-element vector |
Results
| Scenario | const vector& | std::span | absl::Span | Speedup |
|---|---|---|---|---|
| Big Vector | 189 ns | 189 ns | 189 ns | — |
| Small Literal | 13.3 ns | 0.28 ns | 0.28 ns | ~48x |
| C Array | 50.6 ns | 18.3 ns | 18.3 ns | ~2.8x |
| Subrange | 148 ns | 95.9 ns | 95.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>&:
- If you are on C++20, replace with
std::span<const T>. - If you are on C++17 with Abseil, replace with
absl::Span<const T>. - 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.