std::span / gsl::spanstd::span / gsl::spanstd::span

    Replaces (Pointer, Length) Pairs Replaces (Pointer, Length) What for?

    • lightweight (= cheap to copy, can be passed by value)
    • non-owning view (= not responsible for allocating or deleting memory)
    • of a contiguous memory block (of e.g., std::vector, C-array, …)
    span<int> sequence of integers whose values can be changed
    span<int const> sequence of integers whose values can't be changed
    span<int,5> sequence of exactly 5 integers (number of values fixed at compile time)
    void foo(span<int> s); void foo(int* a, int n);
    • well-expressed intent:
      foo accesses a sequence of ints and doesn't take ownership of memory
    • ambiguous semantics:
      does foo take ownership of the array (delete it afterwards)?
    • 1 parameter per data source ⇒ easy to call correctly
    • 2 parameters per data source ⇒ unnecessary confusion, especially when functions take multiple arrays
    • foo's implementation can use standard library conforming interface:
      empty, size, range-based for, begin, end, …
    • ugly and error-prone code inside foo that is specific to handling arrays: 2 parameters whose values can vary independently, nullptr-checks, …

    Using Spans Using

    #include <span>
    #include <gsl/span> +

    As Parameter (Primary Use Case)

    void print_ints  (span<int const> s);
    void print_chars (span<char const> s);
    void modify_ints (span<int> s);
    std::vector<int> v {1,2,3,4};
    print_ints( v );  
    
    std::array<int,3> a {1,2,3}; print_ints( a );
    std::string s = "Some Text"; print_chars( s );
    std::string_view sv = s; print_chars( sv );
    std::vector<int> v {1,2,3,4,5,6,7,8};
    
    // iterator range: print_ints( {begin(v)+2, end(v)} ); print_ints( {begin(v)+2, begin(v)+5} );
    // iterator + length: print_ints( {begin(v)+2, 3} );
    int a[100];  
    print_ints( a );
    auto n = get_number_of_elements();
    auto p = get_pointer_to_memory();
    print_ints( {p, n} );

    A span decouples the storage strategy for sequential data from code that only needs to access the elements in the sequence, but not alter its structure.

    Size & Data Access

    std::span<int> s = ;
    if (s.empty()) return;
    if (s.size() < 1024) {  }
    // spans in range-based for loops
    for (auto x : s) {  }
    
    // indexed access s[0] = 8; if (s[2] > 0) { }
    // iterator access auto m1 = std::min_element(s.begin()s.end()); auto m2 = std::min_element(begin(s)end(s)); auto m3 = std::ranges::min_element(s);

    Comparing Spans

    std::vector<int> v {1,2,3,4};
    std::vector<int> w {1,2,3,4};
    std::span s1 {v};
    std::span s2 {w};
    bool values_same = s1 == s2;  // true
    bool memory_same   = s1.data() == s2.data();  // false

    Spans From Spans

    std::span<> s = ;
    auto first3elements = s.first(3);
    auto last3elements  = s.last(3);
    size_t offset = 2;
    size_t count = 4;
    auto subs = s.subspan(offset, count);
    
    std::span<std::byte const> b = s.as_bytes(); std::span<std::byte> wb = s.as_writable_bytes();

    Making Spans Making Spans Making

    In C++20, std::span's template parameters can be deduced from the constructor arguments.

    std::vector<int>  w {0, 1, 2, 3, 4, 5, 6};
    std::array<int,4> a {0, 1, 2, 3};
    
    std::span sw1 { w }; std::span sa1 { a }; // explicit read-only view: std::span sw2 { std::as_const(w) };
    gsl::span<int> sw3 { w }; gsl::span<int,4> sa2 { a }; // explicit read-only view: gsl::span<int const> sw4 { a };
    std::vector<int> w {0, 1, 2, 3, 4, 5, 6};
                              |----.------'
    std::span      s1 {begin(w)+2, 5};  
    std::span      s2 {begin(w)+2, end(w)}; 
    
    gsl::span<int> s3 {begin(w)+2, 5};  
    gsl::span<int> s4 {begin(w)+2, end(w)};
    int a[100];  
    // auto-deduces type + size
    std::span s1 { a };  
    
    // no parameter deduction gsl::span<int> s2 { a }; gsl::span<int,100> s2 { a }; // constexpr size
    auto len = get_length_at_runtime();
    auto p = std::make_unique<int[]>(len);  
    std::span      s1 {p.get(), len};  
    gsl::span<int> s2 {p.get(), len};  

    Careful!

    std::span s;
    if (  ) {
      std::vector<int> w {1,2,3,4,5};
      s = std::span(w);
    } // w destroyed!
    cout << s[0]; //  UNDEFINED BEHAVIOR: w's memory destroyed!

    Containers like vector might allocate new memory thus invalidating all views of it.

    std::vector<int> w {1,2,3,4,5};
    std::span s {w};
    w.insert(begin(w), {-1,-2,0});
    cout << s[0]; //  UNDEFINED BEHAVIOR: w might hold new memory

    Usage Guidelines Guidelines

    Primary Use Case: As Function Parameter As Function Parameter As Parameter

    • span decouples function implementations from the data represenation / container type
    • clearly communicates the intent of only reading/altering elements in a sequence, but not modifying the underlying memory/data structure
    • makes it easy to apply functions to sequence subranges
    • can almost never be dangling, i.e., refer to memory that has already been destroyed (because parameters outlive all function-local variables)
    • a span's target cannot invalidate the memory that the span refers to during the function's execution (unless it is done in another thread)
    int foo(std::span<int const> in) { … }
    
    std::vector<int> v {…}; foo(v); foo({begin(v), 5}); // even passing a temporary vector is ok // because it will outlive the view parameter: foo(std::vector<int>{1,2,3,4});

    Avoid Returning Spans Avoid Returning

    • easy to produce dangling spans
    • not always clear what object/memory the span refers to
    // which parameter is the span's target?
    std::span<int const>
    foo(std::vector<int> const& x, std::vector<int> const& y);
    // we can assume that the returned span
    // refers to elements of the vector
    std::span<int const> samples(std::vector<int> const& v, int n);
    // however, this is still problematic:
    auto s = samples(vector<int>{1,2,3,4}, 2);
    //  's' is dangling - vector object already destroyed!
    class Payments { 
    public:
      std::span<Money const> of(Customer const&) const;
      
    };
    
    Customer const& john = …; Payments pms = read_payments(file1); auto m = pms.of(john); pms = read_payments(file2); // depending on the implementation of Payments // m's target memory might no longer be valid // after the assignment

    Avoid Local Span Variables Avoid As Variables

    • easy to produce dangling spans, because we have to manually track lifetimes to ensure that no span outlives its target
    • even if the memory owner is still alive, it might invalidate the memory which a span is referring to
    std::vector<int> v1 {1,2,3};
    std::span s {v1};
    if (  ) {
      std::string v2 = std::vector<int>{4,5,6};
      s = v2;
    }
    cout << s; //  v2 already destroyed!