Skip to content

Latest commit

 

History

History
221 lines (164 loc) · 10.2 KB

stdspan.md

File metadata and controls

221 lines (164 loc) · 10.2 KB

std::span


Executive Abstract

std::span<Element, Extent> is a lightweight, non-owning contiguous sequence view in <span> that provides a unified, bounds-aware interface over contiguous memory buffers (std::vector, std::array, raw C-style arrays T[N], or raw pointers). Standardized in C++20 (via proposal P0122R7) and expanded in C++26 with bounds-checked .at(), std::span unifies contiguous memory buffer passing in C++ APIs. It eliminates dangerous raw pointer + length parameter pairs, avoids dynamic memory allocations, supports both runtime dynamic sizing (std::dynamic_extent) and compile-time fixed-extent optimizations (Extent = N), and enables safe raw byte inspection via std::as_bytes.


Motivation & Historical Context

Pre-C++20 Deficiencies: The Pointer-Plus-Length Parameter Anti-Pattern

Prior to C++20, accepting contiguous memory buffers in function parameters forced developers into an architectural tradeoff between rigid container lock-in and dangerous raw pointers:

// Pre-C++20 Option 1: Container Lock-In (Inflexible)
void process_buffer(const std::vector<int>& v);
// Cannot accept std::array<int, 10> or raw C-arrays without copying memory into a std::vector!

// Pre-C++20 Option 2: Raw Pointer + Size Pair (Vulnerable!)
void process_buffer(const int* ptr, std::size_t count);
process_buffer(buffer_a, buffer_b_size); // DANGEROUS BUG: Mis-matched array pointer and length!

Deficiencies of Pre-C++20 Contiguous Buffer Passing:

  1. Pointer/Length Mismatch Hazards: Passing raw pointer and size integer arguments separately invited bugs where callers passed the pointer of array $A$ alongside the size of array $B$, triggering out-of-bounds memory corruption.
  2. Container Type Fragmentation: A function taking const std::vector<T>& rejected std::array<T, N> or stack-allocated arrays T[N], forcing developers to write duplicate template overloads or perform expensive heap-allocating copy operations.
  3. Inability to Enforce Fixed-Size Buffer Constraints: C++ lacked a clean mechanism to declare a function parameter requiring exactly 1024 contiguous elements at compile time without complex array-reference syntax (void f(int (&arr)[1024])).

Solution: ISO C++20 introduced std::span<T, Extent>, encapsulating contiguous memory pointers and length bounds in a small 1-word or 2-word stack object with $\mathcal{O}(1)$ copy and subspan slicing operations.


Technical Mechanics & Extent Architecture

1. Dynamic Extent vs. Static Extent Memory Layout

                 1. Dynamic Extent: std::span<T, std::dynamic_extent> (16 Bytes)
 ┌────────────────────────────────────────┬────────────────────────────────────────┐
 │ T* m_data                              │ std::size_t m_size                     │
 ├────────────────────────────────────────┼────────────────────────────────────────┤
 │ Pointer to contiguous buffer           │ Runtime element count (m_size)         │
 └────────────────────────────────────────┴────────────────────────────────────────┘

                 2. Static Extent: std::span<T, N> (8 Bytes - Zero Size Member!)
 ┌────────────────────────────────────────┐
 │ T* m_data                              │  (Size N is embedded in the class TYPE!
 ├────────────────────────────────────────┤   sizeof(span<T, N>) == sizeof(void*))
 │ Pointer to contiguous buffer           │
 └────────────────────────────────────────┘
  • Dynamic Extent (std::dynamic_extent): Default mode (std::span<T>). Stores a runtime pointer and size member (sizeof(span) == 2 * sizeof(void*)).
  • Static Extent (Extent = N): Fixed-size span (std::span<T, 1024>). The size $N$ is encoded into the C++ type. The compiler omits the size integer field, reducing span object memory size to a single 8-byte pointer!

2. Pass-by-Value Efficiency

Because std::span is a lightweight non-owning view (8 to 16 bytes), it fits directly into CPU registers. Always pass std::span by value (void process(std::span<const int> buf)), rather than by reference (const std::span<int>&).


3. C++26 Bounds-Checked Access (std::span::at())

Method C++ Standard Execution Behavior on Out-of-Bounds ($i \ge \text{size()}$)
span[i] C++20 / C++23 / C++26 Unchecked. Triggers Undefined Behavior in release mode.
span.at(i) C++26 (P2821R5) Checked. Throws std::out_of_range exception safely.

Evolution Across Standards (C++20 to C++26/C++29)

timeline
    title std::span Evolution
    C++20 : Introduced std::span<T, Extent> and std::dynamic_extent P0122R7
          : Added std::as_bytes and std::as_writable_bytes
          : Subspan slicing: .first(k), .last(k), .subspan(offset, count)
    C++23 : Constructor support for ranges std::ranges::borrowed_range
          : Improved initializer_list span construction rules
    C++26 : Introduced std::span::at() P2821R5 for bounds-checked access
          : Native range formatting support for spans in std::print
Loading

Comprehensive Code Examples

Example 1: Unifying Contiguous Buffer Parameters Across Heterogeneous Containers

Demonstrates a single function accepting std::vector, std::array, raw C-arrays, and heap pointers without copying.

#include <iostream>
#include <vector>
#include <array>
#include <span;>

// Function accepts ANY contiguous buffer of integers without allocations or copies!
void print_sum(std::span<const int> buffer) {
    int sum = 0;
    for (int val : buffer) {
        sum += val;
    }
    std::cout << "Buffer Size: " << buffer.size() << " | Sum: " << sum << "\n";
}

int main() {
    // 1. Passing std::vector
    std::vector<int> vec = {10, 20, 30};
    print_sum(vec);

    // 2. Passing std::array
    std::array<int, 4> arr = {1, 2, 3, 4};
    print_sum(arr);

    // 3. Passing raw C-style array
    int raw_arr[] = {100, 200};
    print_sum(raw_arr);

    return 0;
}

Example 2: Static Extent (std::span<T, N>) & Zero-Copy Subspan Slicing

Demonstrates compile-time fixed-size spans and $O(1)$ subspan slicing.

#include <iostream>
#include <array>
#include <span;>

// Requires an EXACT 4-element integer buffer at compile time!
void process_fixed_quad(std::span<const int, 4> quad) {
    std::cout << "Processing 4-element fixed span. First: " << quad[0]
              << " | Last: " << quad[3] << "\n";
}

int main() {
    std::array<int, 6> numbers = {10, 20, 30, 40, 50, 60};

    // O(1) Subspan Slicing: Extract subspan of 4 elements starting at index 1 ({20, 30, 40, 50})
    std::span<const int, 4> fixed_slice = std::span(numbers).subspan<1, 4>();

    process_fixed_quad(fixed_slice); // OK: Types match exactly!
    static_assert(sizeof(fixed_slice) == sizeof(void*), "Static span MUST be 8 bytes!");

    return 0;
}

Example 3: Modern C++26 Bounds-Checked Access (.at()) & std::as_bytes

Demonstrates C++26 std::span::at() and converting typed objects into raw byte views.

#include <iostream>
#include <vector>
#include <span;>
#include <cstddef>
#include <stdexcept>

int main() {
    std::vector<int> data = {101, 202, 303};
    std::span<int> view{data};

    // 1. C++20 std::as_bytes creates std::span<const std::byte> view over object memory!
    std::span<const std::byte> byte_view = std::as_bytes(view);
    std::cout << "Span Total Byte Footprint: " << byte_view.size_bytes() << " bytes\n";

    // 2. C++26 std::span::at() Bounds-Checked Element Access
#if defined(__cpp_lib_span_at) && __cpp_lib_span_at >= 202311L
    try {
        std::cout << "Checked Index 1: " << view.at(1) << "\n";
        int invalid = view.at(10); // Throws std::out_of_range!
    } catch (const std::out_of_range& e) {
        std::cerr << "Caught Expected Span Bounds Exception: " << e.what() << "\n";
    }
#else
    std::cout << "Index 1 (Unchecked): " << view[1] << "\n";
#endif

    return 0;
}

Engineering Best Practices & Common Pitfalls

  1. Pass std::span By Value:

    • Write void process(std::span<const int> buf), not void process(const std::span<int>& buf). std::span is designed to be passed inside CPU registers.
  2. CRITICAL: Lifetime Hazard with Temporary Containers:

    • std::span is a non-owning view. It does not manage container memory lifespans.
    • Dangling Span Danger: Constructing a span over a temporary vector returned by value (std::span<const int> s = get_temp_vector();) creates a span pointing to destroyed memory! Accessing s triggers a Dangling Pointer Use-After-Free Crash.
  3. Use std::span<const T> for Read-Only Buffers:

    • Always qualify element types as const T (std::span<const uint8_t>) when function parameters do not modify buffer contents.

Relationships

  • Related to:
    • std::string_view — Non-owning string view counterpart.
    • std::array — Fixed-size contiguous stack array container.
    • std::byte — Used with std::as_bytes(span) for raw memory inspection.
    • std::span::at (C++26 P2821R5) — Bounds-checked accessor added in C++26.
  • Superseded by: None.
  • Supersedes: Raw pointer + length parameter pairs (const T* ptr, std::size_t len).

Back to C++20 section.