std::span — Non-Owning Views over Arrays
C++17 gave us std::string_view: a non-owning, zero-copy view over a string. C++20 generalised the concept to any contiguous sequence of objects with std::span. It solves the same problem that plague C-style array interfaces — the accidental separation of a pointer from its length — and does so with iterator support, sub-span operations, and full range-based for compatibility, at the cost of exactly two words of storage.
The problem: pointer + size = fragile interface
C-style functions that accept arrays take two parameters: a pointer to the first element and a separate length. The pointer and the length have no enforced relationship — the caller can pass any size, and the callee cannot detect a mismatch at compile time. This pattern also prevents range-based for loops and STL algorithm interoperability, since neither can discover the size from the pointer alone. The traditional C++ improvement, taking const std::vector<T>&, solves the size problem but forces callers to provide a vector specifically — a C array, a std::array, or a raw buffer cannot be passed without copying.
std::span<T> replaces both patterns. It is a lightweight object — typically a pointer and a count — that can be constructed from any contiguous sequence: a C array, a std::vector, a std::array, or a raw pointer+size pair. The size is bundled with the view and travels with it. Functions that take std::span<T> work with every caller that can provide a contiguous sequence of T.
// Old C-style: pointer and size are separate — mismatch possible
void process_old(int* data, size_t count) { /* ... */ }
// Slightly better, but caller must have a vector:
void process_vec(const std::vector<int>& v) { /* ... */ }
// C++20 ideal: accepts any contiguous sequence, size is bundled
void process(std::span<int> data) { /* ... */ }
// All of these work:
int arr[] = {1, 2, 3, 4, 5};
std::vector<int> vec = {1, 2, 3, 4, 5};
std::array<int, 5> sa = {1, 2, 3, 4, 5};
process(arr); // C array — deduces size at compile time
process(vec); // vector — uses vec.data() and vec.size()
process(sa); // std::array — same
process({arr, 3}); // explicit: first 3 elements of arrWhat std::span is (and is not)
std::span is not a container. It does not own, allocate, or manage the memory it refers to. It is a lightweight descriptor — typically implemented as a pointer to the first element and a count of elements — that gives you a window into memory that is owned elsewhere. As Bancila explains in Modern C++ Programming Cookbook, the type was originally called array_view during standardisation, which would have made its nature clearer by analogy with string_view. The rename to span reflects usage in systems programming where the concept is common.
Because span stores only a pointer and a length, it fits in two words. This means it should always be passed by value, not by const&. Passing a const span<T>& adds indirection for no benefit — the span itself is already a non-owning reference to the data, and it is cheap to copy.
#include <span>
// ✓ Pass by value — two words, cheap to copy
void sum_elements(std::span<const int> data) { /* ... */ }
// ✗ Unnecessary indirection — don't do this
void sum_elements(const std::span<const int>& data) { /* ... */ }
// span does NOT own its elements:
std::span<int> dangling;
{
std::vector<int> v = {1, 2, 3};
dangling = v; // span points into v
}
// dangling is now invalid — the vector was destroyed!
// span has the same lifetime hazard as string_view and raw pointersStatic extent vs. dynamic extent
The span template takes an optional second parameter — the extent — that specifies the number of elements. When the extent is provided as a compile-time constant, the span is called a static-extent span: the size is baked into the type and no runtime storage is needed for it. When the extent is omitted (or set to std::dynamic_extent), the count is stored at runtime. The default — and by far the most common form — is a dynamic-extent span because it accepts sequences of any size.
int arr[] = {1, 1, 2, 3, 5, 8, 13};
// Dynamic extent (default): size determined at runtime
std::span<int> dyn{arr};
// sizeof(dyn) == 2 * sizeof(void*) (pointer + count)
// Static extent: size is part of the type — only 1 word needed (just the pointer)
std::span<int, 7> stat{arr};
// sizeof(stat) == sizeof(int*) (no count needed — it's in the type)
// Static extent enforced at compile time:
std::span<int, 5> wrong{arr}; // ✗ ERROR: array has 7 elements, span says 5Static-extent spans are less common but useful when the size is genuinely fixed — for example, when wrapping a hardware register block of a known size, or when processing fixed-width records. For general-purpose interfaces, prefer the dynamic-extent form.
// Function that always processes exactly 3 elements (e.g. RGB pixel)
void apply_gamma(std::span<float, 3> rgb) {
for (auto& channel : rgb)
channel = std::pow(channel, 1.0f / 2.2f);
}
std::array<float, 3> pixel = {0.5f, 0.8f, 1.0f};
apply_gamma(pixel); // ✓ exact match
std::array<float, 4> rgba = {0.5f, 0.8f, 1.0f, 1.0f};
apply_gamma(rgba); // ✗ compile error: extent mismatch (4 vs 3)
apply_gamma(std::span<float, 3>{rgba.data(), 3}); // ✓ explicit sub-spanInterface overview
std::span provides the full set of sequence access operations. Unlike string_view, which is always read-only, span<T> gives mutable access to its elements — modifications through the span change the underlying data. Use span<const T> for read-only access.
| Member | What it does |
|---|---|
| begin() / end() | Mutable iterators over the span |
| cbegin() / cend() | Const iterators over the span |
| rbegin() / rend() | Reverse mutable iterators |
| front() / back() | Access first / last element (by reference) |
| operator[] | Random access by index — no bounds checking |
| data() | Returns raw pointer to first element |
| size() | Number of elements in the view |
| size_bytes() | Total size of the viewed data in bytes |
| empty() | True if size() == 0 |
| first(N) | Returns a span of the first N elements |
| last(N) | Returns a span of the last N elements |
| subspan(offset, count) | Returns a span starting at offset; count optional |
int arr[] = {1, 1, 2, 3, 5, 8, 13};
std::span<int> s{arr};
// Element access
s.front(); // 1
s.back(); // 13
s[3]; // 3
s.data(); // raw int* pointer to arr
// Size information
s.size(); // 7
s.size_bytes(); // 7 * sizeof(int) = 28
// Sub-spans — all return spans into the same memory
auto first3 = s.first(3); // {1, 1, 2}
auto last3 = s.last(3); // {5, 8, 13}
auto mid = s.subspan(2, 3); // {2, 3, 5} — offset 2, count 3
// Range-based for — works exactly like a container
for (int val : s)
std::cout << val << ' '; // 1 1 2 3 5 8 13Mutable vs. read-only spans
A span<T> provides read-write access to the underlying elements — writing through s[i] or iterators modifies the original data. This is unlike string_view, which is inherently read-only. To provide a read-only view — for example in a function that must not modify the data — use span<const T>. A span<T> (mutable) implicitly converts to span<const T> (read-only), so callers holding mutable spans can freely pass them to const-span parameters.
std::vector<int> data = {1, 2, 3, 4, 5};
// Mutable span: modifications affect data
std::span<int> mut{data};
mut[0] = 99; // data[0] is now 99
// Read-only span: cannot modify elements through it
std::span<const int> ro{data};
// ro[0] = 0; // ✗ compile error — const
// Implicit conversion: mutable → const
void read_only(std::span<const int> s);
read_only(mut); // ✓ mutable span implicitly converts to const span
// Use span<const T> in function parameters when the function doesn't mutate:
int sum(std::span<const int> s) {
int total = 0;
for (int v : s) total += v;
return total;
}
// Use span<T> when the function does mutate:
void scale(std::span<float> s, float factor) {
for (auto& v : s) v *= factor;
}What span can and cannot view
std::span requires a contiguous sequence — elements must be laid out adjacently in memory so that a pointer and count fully describe them. This constraint is met by C arrays, std::vector, std::array, and raw pointer+size pairs. It is not met by node-based containers like std::list, std::map, or std::set, which store elements in individually heap-allocated nodes scattered across memory. You also cannot create a span over a std::deque, since its storage is a segmented array of chunks rather than a single block.
// ✓ Contiguous — span works
int c_arr[] = {1, 2, 3}; std::span<int> s1{c_arr};
std::vector<int> vec{1, 2, 3}; std::span<int> s2{vec};
std::array<int, 3> sa{1, 2, 3}; std::span<int> s3{sa};
int* raw = new int[3]{1,2,3}; std::span<int> s4{raw, 3};
// ✗ Non-contiguous — span cannot view these
std::list<int> lst{1, 2, 3};
// std::span<int> bad{lst}; // ✗ does not compile
std::deque<int> dq{1, 2, 3};
// std::span<int> bad2{dq}; // ✗ does not compileByte-level access with as_bytes
Two non-member functions let you reinterpret a span as a view of its raw bytes. std::as_bytes(s) returns a span<const std::byte> over the same memory; std::as_writable_bytes(s) returns a span<std::byte> for write access (only available for non-const spans). These are useful when interfacing with I/O, cryptographic, or serialisation APIs that work with raw bytes rather than typed objects.
std::array<int, 4> values = {1, 2, 3, 4};
std::span<int> s{values};
// Read-only byte view (const int span → const byte span)
auto bytes = std::as_bytes(s);
// bytes.size() == 4 * sizeof(int) == 16
// Writable byte view (non-const span → writable byte span)
auto writable_bytes = std::as_writable_bytes(s);
// Useful for serialisation/network I/O
void send(std::span<const std::byte> buf);
send(std::as_bytes(s)); // send the raw int values over a network socketspan vs. string_view — the key differences
| Property | std::string_view | std::span<T> |
|---|---|---|
| C++ version | C++17 | C++20 |
| Element type | char / wchar_t only | Any type T |
| Mutability | Always read-only | Mutable (use span<const T> for read-only) |
| Owns elements? | No | No |
| Storage | Pointer + size | Pointer + size |
| Pass by | Value | Value |
| String-specific ops | find, substr, compare, starts_with… | None — general sequence ops only |
| Can view non-contiguous? | No | No |
Summary
Replace (T*, size_t) function parameters with std::span<T>
The span bundles pointer and count. Callers can pass any contiguous sequence without modification.
Pass span by value, never by const reference
A span is already a non-owning reference. It is two words — cheaper or equal to passing a pointer+size pair.
Use span<const T> for read-only access
span<T> allows mutation. span<const T> forbids it. A mutable span implicitly converts to a const span.
Use first(), last(), subspan() to pass parts of a sequence
No copies. All return lightweight span views into the same memory.
Span does not extend lifetime
A span over a local vector or array is invalid after that object is destroyed. Same hazard as string_view.
Cannot span non-contiguous containers
list, map, set, deque — none work with span. Only vector, array, C array, and raw pointer+size.