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:

  1. 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 count
  2. Static 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* ptr

    In this scenario, std::span eliminates 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(), and rend() for seamless compatibility with standard library algorithms and range-based for loops.
  • Subviews: Provides .subspan(offset, count), .first(count), and .last(count) member functions. These operations simply compute a new starting pointer and length, returning a new std::span in 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.