Skip to content
C++

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 arr

What 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 pointers

Static 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 5

Static-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-span

Interface 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.

MemberWhat 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 13

Mutable 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 compile

Byte-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 socket

span vs. string_view — the key differences

Propertystd::string_viewstd::span<T>
C++ versionC++17C++20
Element typechar / wchar_t onlyAny type T
MutabilityAlways read-onlyMutable (use span<const T> for read-only)
Owns elements?NoNo
StoragePointer + sizePointer + size
Pass byValueValue
String-specific opsfind, substr, compare, starts_with…None — general sequence ops only
Can view non-contiguous?NoNo

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.

Sign in to track progress