Skip to content
C++
Library
since C++20
Basic

std::span

Non-owning view over a contiguous sequence of objects. C++20's type-safe replacement for (pointer, length) pairs, working with arrays, vectors, std::array, and raw buffers.

std::spansince C++20

A non-owning, lightweight view over a contiguous sequence of T objects with an optional compile-time extent, providing safe array abstraction without ownership or allocation.

Overview

std::span<T, Extent> solves a decades-old problem in C++ APIs: functions that need to accept "an array of T" have historically been forced to choose between unsafe (T*, size_t) pairs, artificial coupling to std::vector<T>, or an explosion of overloads. A span accepts all contiguous storage β€” C-style arrays, std::array, std::vector, std::string, and raw pointer-length pairs β€” without copying, allocating, or coupling to any specific container type.

The second template parameter Extent defaults to std::dynamic_extent, a sentinel value indicating the size is tracked at runtime. When set to an explicit compile-time constant N, the span stores only a pointer β€” sizeof(std::span<T, N>) == sizeof(T*) β€” and the size becomes a compile-time constant. This zero-overhead variant is useful in embedded, numeric, and protocol code where array dimensions are fixed by design.

Spans are value types. Copying a std::span copies two machine words (pointer + size for dynamic; one pointer for static). Pass them by value in function parameters β€” never by const reference.

std::span was introduced in C++20 (<span>). C++23 added at() for bounds-checked element access and construction of std::span<const T> from std::initializer_list<T>.

Syntax

cpp
// Primary template
template<class T, std::size_t Extent = std::dynamic_extent>
class std::span;                                                // C++20

// Construction β€” all produce non-owning views
std::span<T>       s1(ptr, count);          // pointer + element count
std::span<T>       s2(first, last);         // iterator pair
std::span<T>       s3(arr);                 // C-style array β€” extent deduced
std::span<T, N>    s4(std_array);           // std::array<T,N> β€” static extent N
std::span<T>       s5(vec);                 // std::vector<T> β€” dynamic extent
std::span<const T> s6({1, 2, 3});           // initializer_list (C++23, const T only)

// Subspan operations β€” compile-time vs runtime extent
auto a = s.first(n);                        // span<T>    β€” dynamic
auto b = s.first<N>();                      // span<T, N> β€” static
auto c = s.last(n);                         // span<T>    β€” dynamic
auto d = s.last<N>();                       // span<T, N> β€” static
auto e = s.subspan(offset, count);          // span<T>    β€” dynamic
auto f = s.subspan<Offset, Count>();        // span<T, Count> β€” static

// Byte-level views (free functions)
std::span<const std::byte> rb = std::as_bytes(s);           // always available
std::span<std::byte>       wb = std::as_writable_bytes(s);  // T must be non-const

Const correctness: two very different types

cpp
std::vector<int> v = {1, 2, 3};

std::span<int>         s1 = v;   // mutable view β€” can write through s1
std::span<const int>   s2 = v;   // read-only view β€” implicit conversion from span<int>
const std::span<int>   s3 = v;   // span can't be reseated, but elements ARE mutable

// Analogy:
//   span<const T>  β‰ˆ  const T*    β€” data is const, span itself can be rebound
//   const span<T>  β‰ˆ  T* const    β€” span can't be rebound, data is mutable

std::span<T> implicitly converts to std::span<const T>; the reverse is not allowed. Always prefer span<const T> for read-only parameters.

Examples

Unified function parameter replacing multiple overloads

cpp
#include <span>
#include <numeric>
#include <vector>
#include <array>

// One function works for every contiguous container β€” no copies, no templates
double mean(std::span<const double> data) {
    if (data.empty()) return 0.0;
    return std::reduce(data.begin(), data.end()) / static_cast<double>(data.size());
}

void demo() {
    double arr[]                  = {1.0, 2.0, 3.0};
    std::array<double, 4>  a      = {4.0, 5.0, 6.0, 7.0};
    std::vector<double>    v      = {8.0, 9.0, 10.0};

    mean(arr);         // OK β€” C-array, no decay
    mean(a);           // OK β€” std::array
    mean(v);           // OK β€” std::vector
    mean({v.data(), 2}); // OK β€” first two elements, no copy
}

Binary protocol framing with static-extent subspans

cpp
#include <span>
#include <cstdint>
#include <cstring>
#include <stdexcept>

struct PacketHeader {
    uint16_t magic;
    uint16_t length;
};

void parsePacket(std::span<const std::byte> buf) {
    constexpr std::size_t kHeaderSize = sizeof(PacketHeader);
    if (buf.size() < kHeaderSize)
        throw std::runtime_error("truncated header");

    // Static-extent subspan β€” size checked at compile time
    auto header_bytes = buf.first<kHeaderSize>();   // span<const byte, 4>
    PacketHeader hdr{};
    std::memcpy(&hdr, header_bytes.data(), kHeaderSize);

    if (hdr.magic != 0xABCD)
        throw std::runtime_error("bad magic");

    auto payload = buf.subspan(kHeaderSize, hdr.length);
    // process payload...
}

In-place numeric algorithm

cpp
#include <span>
#include <algorithm>
#include <ranges>   // C++20

void clampInPlace(std::span<float> values, float lo, float hi) {
    // std::span satisfies std::ranges::contiguous_range (C++20)
    std::ranges::clamp(values, lo, hi);  // hypothetical β€” use loop for clarity:
    for (auto& v : values)
        v = std::clamp(v, lo, hi);
}

void normalizeInPlace(std::span<float> values) {
    if (values.size() < 2) return;
    auto [mn, mx] = std::ranges::minmax(values);  // C++20
    float range = mx - mn;
    if (range == 0.0f) return;
    for (auto& v : values) v = (v - mn) / range;
}

Bounds-checked access (C++23)

cpp
#include <span>
#include <stdexcept>

void safeRead(std::span<const int> data, std::size_t idx) {
    // operator[] β€” no bounds check, UB if idx >= size()
    int unsafe = data[idx];

    // at() β€” throws std::out_of_range if idx >= size()  (C++23)
    try {
        int safe = data.at(idx);
    } catch (const std::out_of_range& e) {
        // handle
    }
}

Raw memory serialisation with as_bytes

cpp
#include <span>
#include <cstddef>
#include <cstring>

// Write any trivial type into a byte buffer
template<class T>
requires std::is_trivially_copyable_v<T>   // C++20 concept constraint
std::size_t serialise(const T& value, std::span<std::byte> out) {
    if (out.size() < sizeof(T)) return 0;
    std::memcpy(out.data(), &value, sizeof(T));
    return sizeof(T);
}

// Read the object back
template<class T>
requires std::is_trivially_copyable_v<T>
T deserialise(std::span<const std::byte> src) {
    if (src.size() < sizeof(T))
        throw std::runtime_error("buffer too small");
    T value{};
    std::memcpy(&value, src.data(), sizeof(T));
    return value;
}

Best Practices

Pass by value, not by reference. A std::span is at most two machine words. void f(std::span<const int> s) is cheaper than void f(const std::span<const int>& s).

Prefer span<const T> for read-only parameters. It communicates intent, prevents accidental writes, and allows callers to pass const-qualified containers.

Use static extent when size is compile-time known. std::span<float, 4> produces a single-pointer type with no runtime size bookkeeping. Compilers can additionally verify argument sizes at compile time in constrained contexts.

Don't store spans as class members without careful lifetime analysis. A span does not extend the lifetime of its referent. If your class needs to own data, store the container β€” not the view.

Use std::as_bytes for type-agnostic I/O. Instead of reinterpret-casting buffers, convert to span<const std::byte> via std::as_bytes. It's well-defined and signals intent clearly.

Validate size before constructing a fixed-extent span from a dynamic source. The constructor std::span<T, N>(ptr, count) has a precondition that count == N; violating it is undefined behaviour.

Common Pitfalls

Dangling after container reallocation

cpp
std::vector<int> v = {1, 2, 3};
std::span<int>   s = v;

v.push_back(4);   // may reallocate β€” s.data() now points to freed memory
s[0] = 99;        // UB

A span captures the address of the underlying storage at construction time. Any operation that invalidates a container's iterators also invalidates any span over that container.

Returning a span to local storage

cpp
std::span<int> bad() {
    int arr[] = {1, 2, 3};
    return std::span(arr);   // UB β€” arr destroyed on return
}

std::span<int> also_bad() {
    std::vector<int> v = {1, 2, 3};
    return std::span(v);     // UB β€” v destroyed on return
}

const span<T> does not mean read-only

cpp
const std::span<int> s = v;
s[0] = 42;   // compiles and works β€” the SPAN is const, not the ints

If your intent is a read-only view, the type must be std::span<const int>.

Not null-terminated β€” unsafe with C APIs

cpp
std::span<const char> s = getSomeChars();
printf("%s", s.data());   // dangerous β€” no guarantee of null terminator

Use std::string_view for character sequences that must interact with null-terminated C APIs.

Fixed-extent vs dynamic-extent spans

The Extent template parameter is the key design choice when using std::span. A dynamic-extent span (std::dynamic_extent) stores a pointer and a size_t, while a fixed-extent span of size N stores only a pointer β€” the size is baked into the type.

cpp
#include <span>
#include <array>

// Dynamic extent β€” size tracked at runtime, sizeof == 2 * sizeof(void*)
std::span<int>           dyn;                 // C++20
static_assert(sizeof(dyn) == 2 * sizeof(void*));

// Static extent β€” size is part of the type, sizeof == sizeof(void*)
std::span<int, 4>        fixed;               // C++20
static_assert(sizeof(fixed) == sizeof(void*));

// Deduced static extent from std::array
std::array<int, 4> arr = {1, 2, 3, 4};
std::span<int, 4>  s = arr;                   // extent=4 deduced or explicit

// Converting static→dynamic is always implicit; dynamic→static is not
std::span<int>     dyn2 = s;                  // OK β€” widening conversion
// std::span<int, 4> bad = dyn2;              // Error β€” runtime size unknown

Static-extent spans are especially valuable in embedded or SIMD code where wrong-size arguments should be caught at compile time rather than silently proceeding with a runtime check.

cpp
// Enforces at compile time that callers pass exactly 4 floats      // C++20
void processQuad(std::span<float, 4> quad) {
    quad[0] *= 2.0f;
    quad[1] *= 2.0f;
    quad[2] *= 2.0f;
    quad[3] *= 2.0f;
}

std::array<float, 4> v4 = {1, 2, 3, 4};
processQuad(v4);                  // OK
// processQuad(someVector);       // compile error β€” size unknown at compile time

Subspans: first, last, subspan

Every subspan operation has both a runtime form and a template form that preserves the static extent. Prefer the template form whenever the sub-size is a compile-time constant; it propagates the size into the type and enables the same zero-overhead properties as a static-extent span.

cpp
#include <span>
#include <cstdint>

void demonstrate(std::span<const uint8_t> buf) {           // C++20
    // Runtime forms β€” result has dynamic extent
    auto head3 = buf.first(3);              // span<const uint8_t>
    auto tail2 = buf.last(2);               // span<const uint8_t>
    auto mid   = buf.subspan(3, 4);         // span<const uint8_t>, offset=3 count=4

    // Template forms β€” result carries size in its type
    auto head3s = buf.first<3>();           // span<const uint8_t, 3>
    auto tail2s = buf.last<2>();            // span<const uint8_t, 2>
    auto mids   = buf.subspan<3, 4>();      // span<const uint8_t, 4>

    // Precondition: count/offset must be in-range β€” violation is UB
    // buf.first(buf.size() + 1);           // UB at runtime
    // buf.first<100>();                    // UB if buf.size() < 100
}

A common pattern is peeling a fixed-size header off a raw byte buffer and leaving a variable-length payload:

cpp
#include <span>
#include <cstdint>
#include <cstring>

struct MsgHeader { uint32_t id; uint16_t len; };       // C++20

std::span<const std::byte> parseMessage(std::span<const std::byte> raw) {
    constexpr auto kHdrSz = sizeof(MsgHeader);
    if (raw.size() < kHdrSz) return {};

    auto hdrBytes = raw.first<kHdrSz>();               // span<const byte, 6>
    MsgHeader hdr{};
    std::memcpy(&hdr, hdrBytes.data(), kHdrSz);

    if (raw.size() < kHdrSz + hdr.len) return {};
    return raw.subspan(kHdrSz, hdr.len);               // span<const byte>
}

span over 2D data

std::span is inherently one-dimensional, but a flat span over 2D data is idiomatic C++ for row-major matrices. Pass the full buffer plus dimensions separately, or encode them in the type via a helper wrapper.

cpp
#include <span>
#include <vector>
#include <cstddef>

// Row-major matrix view: element [r][c] = data[r * cols + c]        // C++20
void addScalar(std::span<float> data, std::size_t rows,
               std::size_t cols, float k) {
    for (std::size_t r = 0; r < rows; ++r)
        for (std::size_t c = 0; c < cols; ++c)
            data[r * cols + c] += k;
}

// Extract a single row as a span β€” zero-copy sub-view
std::span<float> row(std::span<float> data, std::size_t cols,
                     std::size_t r) {
    return data.subspan(r * cols, cols);
}

void demo() {
    std::vector<float> matrix(3 * 4, 0.0f);           // 3Γ—4 row-major
    addScalar(matrix, 3, 4, 1.0f);

    auto r1 = row(matrix, 4, 1);                       // view of row 1
    r1[0] = 99.0f;                                     // writes back into matrix
}

C++23's std::mdspan (from <mdspan>) is the proper multidimensional view type; use a flat std::span only when you need single-dimension access or when targeting C++20 without mdspan.

cpp
// C++23: mdspan as the multidimensional counterpart
#include <mdspan>

std::vector<float> buf(3 * 4);
std::mdspan<float, std::extents<std::size_t, 3, 4>> mat(buf.data()); // C++23
mat[1, 2] = 42.0f;                                    // row 1, col 2

span and contiguous_range concept

std::span<T> satisfies std::ranges::contiguous_range, std::ranges::sized_range, and std::ranges::borrowed_range, making it a first-class citizen of the C++20 ranges machinery. Any range algorithm that requires contiguous, sized, or borrowed ranges can accept a span directly.

cpp
#include <span>
#include <algorithm>
#include <ranges>
#include <concepts>

static_assert(std::ranges::contiguous_range<std::span<int>>);   // C++20
static_assert(std::ranges::sized_range<std::span<int>>);
static_assert(std::ranges::borrowed_range<std::span<int>>);

// Because span is a borrowed_range, algorithms can safely return iterators
// into it without dangling-reference problems
std::vector<int> v = {3, 1, 4, 1, 5, 9};
std::span<int>   s = v;

auto it = std::ranges::find(s, 4);                              // C++20
if (it != s.end())
    *it = 0;                                                    // writes back into v

The borrowed_range property is critical: it tells the ranges library that an iterator into the span remains valid after the span object itself is destroyed (the span is just a view β€” the underlying storage persists). This makes it safe to pass a span as a temporary to algorithms that return iterators.

cpp
#include <span>
#include <algorithm>
#include <ranges>
#include <vector>

// span as a temporary β€” safe because it is a borrowed_range            // C++20
std::vector<int> data = {5, 3, 1, 4, 2};
auto it = std::ranges::min_element(std::span{data});
// it is valid β€” refers into data, not into the (now-destroyed) temporary span

span vs array_view and gsl::span

std::span standardises the concept pioneered by gsl::span (Guidelines Support Library) and array_view from early C++ Core Guidelines work. If you are migrating a codebase that used either predecessor, the interfaces are nearly identical with a few naming differences.

Featuregsl::span (pre-C++20)std::span (C++20)
Header<gsl/span><span>
Dynamic sentinelgsl::dynamic_extentstd::dynamic_extent
Bounds checkingoperator[] throws (configurable)operator[] β€” UB; at() throws (C++23)
as_bytesgsl::as_bytesstd::as_bytes
narrow_cast helpersyesno
zstring_spanyesuse std::string_view
cpp
// Migration: replace gsl::span with std::span                       // C++20
// Before (pre-C++20):
#include <gsl/span>
void process(gsl::span<const int> data);

// After (C++20):
#include <span>
void process(std::span<const int> data);   // typically a drop-in replacement

The main behavioral difference is bounds checking: gsl::span::operator[] could be configured to throw or assert on out-of-bounds access, while std::span::operator[] has undefined behavior. Migrate any call sites relying on GSL's checked operator to use at() (C++23) or an explicit bounds check.

Byte-level access with as_bytes

std::as_bytes and std::as_writable_bytes reinterpret any contiguous span as a view of raw std::byte, enabling type-safe I/O, hashing, and serialisation without reinterpret_cast. The two free functions are the sanctioned way to obtain a byte-level alias of any trivially copyable data.

cpp
#include <span>
#include <cstddef>
#include <cstdint>
#include <array>

struct Sensor { float temperature; uint32_t timestamp; };      // C++20

Sensor reading{36.6f, 1700000000u};

// as_bytes β€” read-only byte view; always available
std::span<const std::byte> rb = std::as_bytes(std::span{&reading, 1});
static_assert(sizeof(Sensor) == 8);
// rb.size() == 8

// as_writable_bytes β€” mutable byte view; requires non-const T
std::span<std::byte> wb = std::as_writable_bytes(std::span{&reading, 1});
wb[0] = std::byte{0xFF};   // overwrites first byte of 'reading'

Use as_bytes over reinterpret_cast for two reasons: it is guaranteed well-defined for any span element type (the standard explicitly blesses byte-aliasing), and the resulting span carries the correct size so callers cannot accidentally read past the end.

cpp
#include <span>
#include <cstddef>
#include <vector>
#include <fstream>

// Write any contiguous container to a binary stream               // C++20
template<class T>
requires std::is_trivially_copyable_v<T>
void writeBinary(std::ofstream& out, std::span<const T> data) {
    auto bytes = std::as_bytes(data);
    out.write(reinterpret_cast<const char*>(bytes.data()),
              static_cast<std::streamsize>(bytes.size()));
}

// Read back
template<class T>
requires std::is_trivially_copyable_v<T>
void readBinary(std::ifstream& in, std::span<T> data) {
    auto bytes = std::as_writable_bytes(data);
    in.read(reinterpret_cast<char*>(bytes.data()),
            static_cast<std::streamsize>(bytes.size()));
}

Note that as_writable_bytes requires the span's element type to be non-const β€” std::as_writable_bytes(std::span<const int>{...}) is a compile error by design.

See Also

  • std::string_view β€” the character-specific analogue; prefer it for text
  • std::vector β€” owning contiguous container; source of many spans
  • std::array β€” fixed-size owning array; maps to a static-extent span