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.

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