How std::span Represents Contiguous Sequences in C++
This article explores how std::span, introduced in
C++20, acts as a lightweight, non-owning view over contiguous sequences
of objects. It details the internal mechanics of std::span,
including its pointer-and-size composition, compile-time versus runtime
extents, and how it safely unifies disparate contiguous storage
types—such as raw arrays, std::vector, and
std::array—under a single, efficient interface without
transferring ownership or incurring allocation costs.
Core Architecture and Mechanics
At its fundamental level, std::span<T> represents
a contiguous sequence of elements of type T using a simple
structure: a pointer to the first element and a length count. Unlike
containers such as std::vector or std::array,
std::span does not own the memory it references. It acts as
an unmanaged view, requiring no memory allocations, deallocations, or
data copying when created or passed around.
Because it relies strictly on contiguous memory layout, every element in the sequence is guaranteed to reside adjacent to the next in physical memory, matching the layout invariants required by standard pointer arithmetic:
*(ptr + i) == ptr[i]Dynamic Extent vs. Static Extent
std::span handles sequence lengths in two distinct ways
via its second template parameter:
Dynamic Extent (
std::dynamic_extent): When the size is unknown at compile time,std::span<T>stores both a pointer to the data and a runtime size (std::size_t). This layout mirrors a standard pointer-length pair:void process(std::span<int> data); // Stores: int* ptr, size_t countStatic Extent: When the size is fixed at compile time, the size is embedded directly into the type:
void process(std::span<int, 4> data); // Stores: only int* ptrIn this scenario,
std::spaneliminates the runtime size member entirely. Its size overhead matches that of a raw pointer (sizeof(void*)), while still exposing member functions like.size()which return a compile-time constant.
Unifying Contiguous Data Sources
std::span provides implicit constructors that accept any
contiguous data structure implementing .data() and
.size(), as well as standard C-style arrays:
- Raw arrays:
int arr[10]; std::span s = arr; - Dynamic containers:
std::vector<int> vec{1, 2, 3}; std::span s = vec; - Static containers:
std::array<int, 4> arr{1, 2, 3, 4}; std::span s = arr; - Direct pointer and size:
std::span s(ptr, count);
This implicit conversion standardizes API design. Instead of
overloading functions for different container types or accepting
error-prone (T* data, size_t size) combinations, a single
std::span<T> parameter safely accepts all contiguous
data structures.
Subsequences and Element Access
std::span enables safe and expressive operations over
contiguous ranges without copying memory:
- Indexing: Access elements using
operator[]without pointer arithmetic syntax. - Iterators: Exposes
begin(),end(),rbegin(), andrend()for seamless compatibility with standard library algorithms and range-basedforloops. - Subviews: Provides
.subspan(offset, count),.first(count), and.last(count)member functions. These operations simply compute a new starting pointer and length, returning a newstd::spanin constant time (\(O(1)\)).
Lifetime Responsibilities
Because std::span does not own the elements it
references, it is subject to dangling references. If the underlying
container is resized, cleared, or goes out of scope (such as a local
std::vector whose capacity reallocates or an array
destroyed at block exit), the std::span becomes invalid.
Developers must ensure the lifetime of the underlying data exceeds the
lifetime of the span viewing it.