std::span — Non-Owning Contiguous View
"C++20 std::span: a zero-overhead view over contiguous data. Fixed vs dynamic extent, subspans, byte reinterpretation, and API design patterns."
std::span<T, Extent>since C++20A non-owning view over a contiguous sequence of objects, storing only a pointer and (for dynamic extent) a size — no allocation, no lifetime management.
Overview
Before C++20, passing a contiguous sequence to a function required choosing between a concrete container type (const std::vector<int>&, which locks callers into heap allocation) or a C-style pair (int*, size_t, which loses type safety and range information). std::span solves this by representing the view abstractly: any contiguous range — raw array, std::array, std::vector, heap buffer — converts implicitly.
The key properties:
- Zero overhead: for dynamic extent, two words (pointer + size); for fixed extent, one word (pointer only, size is a compile-time constant).
- Non-owning: the span never allocates or frees. Lifetime is entirely the caller's responsibility.
- Mutable by default:
std::span<T>gives read-write access; usestd::span<const T>for read-only views. - No decay: unlike C-arrays passed as pointers, a span carries its length through function boundaries.
std::span is to arrays what std::string_view (C++17) is to strings — a lightweight, non-owning handle. A std::string_view is in fact semantically equivalent to std::span<const char> with additional string-specific members.
Syntax
template<typename T, std::size_t Extent = std::dynamic_extent>
class span;Extent is either std::dynamic_extent (the default, size stored at runtime) or a compile-time constant (size baked in, no storage cost).
Construction
#include <span>
int arr[8] = {0, 1, 2, 3, 4, 5, 6, 7};
std::vector<int> vec{10, 20, 30};
std::array<int, 4> sarr{1, 2, 3, 4};
// Dynamic extent — deduced from the argument
std::span<int> s1{arr}; // size = 8
std::span<int> s2{vec}; // size = vec.size()
std::span<int, 4> s3{sarr}; // fixed extent, deduced
// Pointer + count — explicit range
std::span<int> s4{arr + 2, 4}; // {2,3,4,5}
// Iterator pair (C++23)
std::span<int> s5{arr, arr + 5}; // C++23
// Read-only view of mutable data
std::span<const int> cs{vec}; // elements not modifiable through cs
// From another span — implicit narrowing via subspan
std::span<int, 3> fixed = s1.first<3>(); // fixed extent from dynamicFixed vs Dynamic Extent
The Extent template parameter is the central design choice for any span. Understanding the trade-off is essential before reaching for std::span in API design.
Fixed extent (std::span<T, N>): the size is a compile-time constant baked into the type. The span object stores only a pointer — no runtime size field. This means sizeof(std::span<int, 4>) == sizeof(int*). The compiler can verify size mismatches statically, and template machinery can dispatch on the size.
Dynamic extent (std::span<T> or std::span<T, std::dynamic_extent>): the size is a runtime value stored alongside the pointer. sizeof(std::span<int>) == 2 * sizeof(void*) — two words. This is the right default for general APIs where callers supply data of varying lengths.
// C++20
#include <span>
#include <array>
std::array<int, 4> arr4 = {1, 2, 3, 4};
// Fixed extent — size is part of the type
std::span<int, 4> fixed = arr4;
static_assert(fixed.extent == 4); // constexpr member
// sizeof(fixed) == sizeof(int*) // no size word stored
// Dynamic extent — size stored at runtime
std::span<int> dyn = arr4; // implicit conversion from fixed to dynamic
// sizeof(dyn) == 2 * sizeof(void*) // pointer + size
// Conversion rules: fixed → dynamic is implicit, dynamic → fixed is not
std::span<int, 4> back = dyn; // ERROR: cannot implicitly narrow
std::span<int, 4> back2 = std::span<int, 4>{dyn.data(), 4}; // explicit construction
// Fixed extent enables compile-time enforcement
template<typename T, std::size_t N>
void load_matrix_row(std::span<T, N> row) {
static_assert(N == 4 || N == 9 || N == 16,
"only 2×2, 3×3, or 4×4 matrices");
}
load_matrix_row(std::span<int, 4>{arr4}); // OK
// load_matrix_row(dyn); // ERROR: static_assert not reachable
// // because N is not a compile-time valueA fixed-extent span is the right tool when the size is a structural invariant of the protocol or data format — SIMD-width chunks, fixed-size protocol frames, GPU buffer rows. For all general-purpose APIs, dynamic extent is preferable because it accepts containers of any size without a template parameter on the call site.
The extent static member distinguishes the two at compile time:
// C++20
std::span<int, 4>::extent; // 4 (constexpr std::size_t)
std::span<int>::extent; // std::dynamic_extent (a large sentinel value)
if constexpr (S::extent == std::dynamic_extent) {
// runtime-size path
} else {
// compile-time-size path
}Examples
Subspan Operations
Spans support slicing into sub-views without any copy. Both runtime and compile-time variants exist — the template versions preserve the fixed extent in the return type, which is useful when the slice size is a known constant.
// C++20
#include <span>
#include <numeric>
std::vector<int> data(1024);
std::iota(data.begin(), data.end(), 0);
std::span<int> view{data};
// --- Runtime subspans ---
auto head = view.first(32); // first 32 elements → span<int>
auto tail = view.last(32); // last 32 elements → span<int>
auto middle = view.subspan(16, 64); // 64 elements at offset 16 → span<int>
auto rest = view.subspan(128); // from offset 128 to end
// --- Compile-time subspans — return span with fixed extent ---
auto fixed_head = view.first<32>(); // → span<int, 32>
auto fixed_tail = view.last<32>(); // → span<int, 32>
auto fixed_mid = view.subspan<16, 64>(); // → span<int, 64>
// --- Splitting a span in half ---
auto half = view.size() / 2;
auto left = view.first(half);
auto right = view.last(view.size() - half);
// Or as a pair:
auto [lhs, rhs] = std::pair{view.first(half), view.last(view.size() - half)};
// --- Chunked processing ---
constexpr std::size_t CHUNK = 64;
for (std::size_t i = 0; i + CHUNK <= view.size(); i += CHUNK) {
process_chunk(view.subspan(i, CHUNK));
}
// Handle the remainder, if any
if (auto rem = view.size() % CHUNK; rem > 0) {
process_chunk(view.last(rem));
}The template overloads (first<N>(), last<N>(), subspan<Offset, Count>()) are preferable whenever the slice parameters are compile-time constants — the resulting fixed-extent span can participate in further compile-time operations and avoids storing a redundant size word.
std::as_bytes and std::as_writable_bytes
std::as_bytes reinterprets any span as a std::span<const std::byte>, giving a raw byte view of the underlying memory without any explicit reinterpret_cast. This is the standard mechanism for serialization, hashing, and I/O.
// C++20
#include <span>
#include <cstddef> // std::byte
// --- Hashing arbitrary typed data ---
float floatArray[] = {1.0f, 2.0f, 3.0f, 4.0f};
std::span<const std::byte> raw = std::as_bytes(std::span{floatArray});
// raw.size() == 4 * sizeof(float) == 16
// Feed raw bytes to a hash function without casting at every call site
update_hash(raw.data(), raw.size());
// --- Serialising a struct ---
struct Packet { uint32_t id; float value; };
void write_packets(std::ostream& out, std::span<const Packet> packets) {
auto bytes = std::as_bytes(packets); // span<const std::byte>
out.write(
reinterpret_cast<const char*>(bytes.data()),
static_cast<std::streamsize>(bytes.size_bytes())
);
}
// --- In-place mutation with as_writable_bytes ---
void xor_buffer(std::span<float> buf, uint8_t key) { // NOTE: non-const span
for (std::byte& b : std::as_writable_bytes(buf))
b ^= std::byte{key};
}
// as_writable_bytes on a const span is a compile error:
std::span<const float> ro{floatArray};
// std::as_writable_bytes(ro); // ERROR — prevents accidental mutationstd::as_writable_bytes requires the span to be non-const (std::span<T>, not std::span<const T>). This constraint is enforced at compile time and cleanly prevents accidentally writing through a read-only view.
API Design: Replacing Pointer-Length Pairs
The most impactful use of std::span is eliminating the (T* ptr, size_t n) antipattern from public APIs. A single std::span<T> parameter accepts every contiguous container — raw arrays, std::array, std::vector, raw pointers with sizes — without overloads or templates on the call site.
// C++20
#include <span>
#include <cstdint>
// --- Before: two overloads, or a C-style interface ---
void encode_before(const uint8_t* data, std::size_t len);
void encode_before(const std::vector<uint8_t>& data); // separate overload
// --- After: one function, all callers work ---
void encode(std::span<const uint8_t> data) {
// data.data() gives the raw pointer for C API handoff
EVP_DigestUpdate(ctx, data.data(), data.size());
}
// All of these compile without any cast or size arithmetic:
std::vector<uint8_t> heap_buf{0x01, 0x02, 0x03};
uint8_t stack_buf[64]{};
std::array<uint8_t,32> arr_buf{};
encode(heap_buf); // std::vector
encode(stack_buf); // raw C array
encode(arr_buf); // std::array
encode({heap_buf.data() + 4, 16}); // sub-range with explicit pointer+size
encode(std::span{stack_buf}.first(32)); // half the stack buffer
// --- Read-only vs mutable: use const T for readers, T for writers ---
void fill_zeros(std::span<uint8_t> buf) { // writer: needs mutable access
std::fill(buf.begin(), buf.end(), 0);
}
void checksum(std::span<const uint8_t> data) -> uint32_t; // reader: const TAccepting std::span<const T> for read-only functions is important: the const propagates to the elements, not just the span handle. A std::vector<uint8_t> binds to std::span<const uint8_t> implicitly, so callers of read-only functions are never forced to drop const.
Range-Based For and Algorithms
Spans model std::ranges::contiguous_range, so every range algorithm and range-based for loop works directly.
// C++20
#include <span>
#include <algorithm>
#include <ranges>
std::vector<int> v{5, 3, 1, 4, 2};
std::span<int> s{v};
// Range-based for — reference iteration
for (int& x : s) {
x *= 2; // modifies the underlying vector in-place
}
// std::ranges algorithms — no .begin()/.end() boilerplate
std::ranges::sort(s); // sort in-place
auto it = std::ranges::find(s, 6); // find by value
bool any = std::ranges::any_of(s, [](int x){ return x > 8; });
// Pipe syntax with views (span is a range, views compose)
auto even_doubled = s
| std::views::filter([](int x){ return x % 2 == 0; })
| std::views::transform([](int x){ return x * 2; });
// Span satisfies these concepts:
static_assert(std::ranges::contiguous_range<std::span<int>>);
static_assert(std::ranges::sized_range<std::span<int>>);
static_assert(std::contiguous_iterator<std::span<int>::iterator>);Because spans are contiguous ranges, std::span<T>::iterator is a contiguous iterator — the strongest iterator category. Algorithms like std::sort, std::copy, and SIMD-aware routines can exploit this guarantee for maximum performance.
Best Practices
Pass std::span<const T> for read-only access. A function that only reads data should advertise that — callers can pass both mutable and const sources, and the interface is self-documenting.
Don't store spans across potential reallocation boundaries. A span is a snapshot of a pointer at a moment in time. If the underlying std::vector reallocates, the span becomes a dangling pointer with no warning from the compiler:
std::vector<int> v{1, 2, 3};
std::span<int> s{v};
v.push_back(4); // may reallocate — s is now dangling!
s[0] = 99; // UBStore spans only when the lifetime and stability of the backing storage is guaranteed — fixed-size buffers, std::array, non-resizing sections of a vector.
Prefer fixed extent when the size is a structural invariant. It eliminates the runtime size field, enables compile-time bounds reasoning, and communicates intent more clearly than a runtime assertion.
Use data() + size() for C API interop. Spans bridge safely into C interfaces without any cast:
void c_api_call(const float* ptr, int n);
std::span<const float> view{...};
c_api_call(view.data(), static_cast<int>(view.size()));Note that size() returns std::size_t (unsigned) for std::span, consistent with standard containers in C++20. Cast explicitly when a C API expects int or ptrdiff_t.
Common Pitfalls
The const span vs span<const T> Confusion
const std::span<int> makes the span handle const — you cannot rebind it to point elsewhere — but the elements it points to remain fully mutable. std::span<const int> makes the elements read-only through the span. These are orthogonal: you can have either, both, or neither.
// C++20
std::vector<int> v{1, 2, 3};
const std::span<int> cs{v};
cs[0] = 99; // compiles — elements are still mutable
// cs = other; // ERROR — the span handle is const, cannot rebind
std::span<const int> rs{v};
// rs[0] = 99; // ERROR — read-only view, elements not assignable
// Rebindable, read-only:
std::span<const int> rrs{v};
rrs = std::span<const int>{v}; // OK — can rebind
// Immutable handle AND immutable elements:
const std::span<const int> both{v};
// both[0] = 99; // ERROR
// both = other; // ERRORThe table:
| Type | Elements mutable? | Handle rebindable? |
|---|---|---|
std::span<int> | Yes | Yes |
const std::span<int> | Yes | No |
std::span<const int> | No | Yes |
const std::span<const int> | No | No |
Always use std::span<const T> to enforce read-only access at the element level. const std::span<T> is rarely the right choice; it prevents rebinding but still allows accidental element mutation.
Dangling Spans from Temporaries
Span does not extend the lifetime of temporary containers. The temporary is destroyed at the semicolon where it was created, leaving the span pointing at freed memory. The compiler issues no warning.
// C++20
// WRONG: temporary vector is destroyed immediately
std::span<int> bad = std::vector<int>{1, 2, 3};
bad[0]; // UB — points at freed memory
// WRONG: function returns a temporary
std::vector<int> get_vector() { return {10, 20, 30}; }
std::span<int> also_bad = get_vector(); // temporary destroyed at semicolon
also_bad[0]; // UB
// CORRECT: name the container so its lifetime encloses the span's use
std::vector<int> storage{1, 2, 3};
std::span<int> good{storage};
good[0] = 99; // OK — storage is alive
// CORRECT: same pattern for function results
auto container = get_vector();
std::span<int> view{container};This differs from const T&, which the language specifically extends temporaries for. Span has no such special rule. The rule of thumb: a span's lifetime must be strictly nested inside the lifetime of its backing storage.
Returning a span to a local variable from a function is the same class of bug:
std::span<int> get_span() {
std::vector<int> local{1, 2, 3};
return std::span<int>{local}; // UB: local destroyed on return
}Comparison and Equality
std::span has no operator== in C++20. Two spans pointing to the same data are not considered equal by the default operators — there is no element-wise comparison built in. Use std::ranges::equal for content comparison:
std::span<int> a{...}, b{...};
bool same = std::ranges::equal(a, b); // element-wise
bool same_storage = (a.data() == b.data() && a.size() == b.size()); // identityLooking Ahead: std::mdspan (C++23)
std::mdspan is the multi-dimensional generalization of std::span. Where std::span is a 1D view into contiguous memory, std::mdspan adds a layout policy and multiple independent extents — making it suitable for matrices, tensors, and any strided memory layout. It is defined in <mdspan>.
// C++23
#include <mdspan>
float data[12] = { /* row-major 3×4 matrix */ };
// 2D view: 3 rows, 4 columns (row-major layout by default)
std::mdspan<float, std::extents<int, 3, 4>> mat{data};
mat[1, 2] = 42.0f; // row 1, column 2 (C++23 multi-index operator[])
auto rows = mat.extent(0); // 3
auto cols = mat.extent(1); // 4
// Dynamic extents work too
std::mdspan<float, std::dextents<int, 2>> dyn_mat{data, 3, 4};
dyn_mat[0, 0] = 1.0f;
// Custom layout policies: column-major, padded, strided
using col_major = std::layout_left;
std::mdspan<float, std::extents<int, 3, 4>, col_major> col_mat{data};Like std::span, std::mdspan is non-owning and zero-overhead — it is a pure view. The layout policy is a compile-time type parameter, so switching between row-major and column-major incurs no runtime cost. Extent values can be mixed: some dimensions can be compile-time constants while others are dynamic.
std::mdspan is the foundation on which BLAS-style linear algebra in C++ (std::linalg, also C++26) is built. If your code works with 2D arrays or tensors today, it is worth learning mdspan as the canonical way to express those abstractions without hand-rolling pointer arithmetic.
See Also
std::string_view— the string-specific equivalent;std::span<const char>with string operationsstd::mdspan— C++23 multi-dimensional span for matrix and tensor viewsstd::ranges— span satisfiescontiguous_range, works directly with all range algorithms- RAII — contrast with owning containers; span is explicitly the non-owning complement