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

    Replaces (Pointer, Length) Pairs Purpose(s)

    #include <span>
    • 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, …)
    • primary use case: as function parameter  (container-independent access to values)
    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)
    span as a view into vector
    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

    #include <span>C++20
    #include <gsl/span>C++14 + GSL

    As Parameter (Primary Use Case)

    void print_ints  (std::span<int const> s);
    void print_chars (std::span<char const> s);
    void modify_ints (std::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), end(v)} );  
    print_ints( {begin(v)+2, end(v)} );  
    print_ints( {begin(v)+2, begin(v)+5} );  
    // iterator + length:
    print_ints( {begin(v)+2, 3} );  
    
    int main () {
    int a[20];  
    
    print_ints( a );  
    
    // e.g. access to a C library's memory
    size_t n = get_number_of_elements();
    int*   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));

    Comparing Spans

    #include <algorithm>  // std::ranges::equal
    std::vector<int> v {1,2,3,4};
    std::vector<int> w {1,2,3,4};
    std::span sv {v};
    std::span sw {w};
    bool memory_same   = sv.data() == sw.data();  // false
    bool values_same = std::ranges::equal(sv,sw);  // true
    

    Making Spans

    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 }; // C++20 std::span sa1 { a }; // explicit read-only view: std::span sw2 { std::as_const(w) };
    gsl::span<int> sw3 { w }; // GSL 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, 4};  // C++20
    std::span      s2 {begin(w)+2, end(w)}; 
    
    int a[100];  
    // auto-deduces type + size
    std::span s1 { a };  C++20
    
    // no parameter deduction gsl::span<int> s2 { a }; GSL gsl::span<int,100> s2 { a }; // constexpr size
    int len = get_length_at_runtime();
    auto p = std::make_unique<int[]>(len);  
    std::span      s1 {p.get(), len};  C++20
    gsl::span<int> s2 {p.get(), len};  GSL

    Making Spans From Spans

    std::vector<int> v {0,1,2,3,4,5,6,7,8};
    std::span s = v;
    auto first3elements = s.first(3);
    auto last3elements  = s.last(3);
    size_t offset = 2;
    size_t count = 4;
    auto subs = s.subspan(offset, count);
    

    Usage Guidelines

    Primary Use Case: As Function Parameter

    • span decouples function implementations from the data representation / 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)
    • int foo (std::span<int const> in) { … }
      
      std::vector<int> v {…}; // v will always outlive parameter 'in'! foo(v); foo({begin(v), 5});
    • 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)
    • spans can also speed up sequence accesses, by avoiding a level of indirection:

      a const reference to a vector of ints potentially involves two indirections while a span into the vector can point directly at the vector's storage

    Careful When Returning Spans

    • 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> random_subrange (std::vector<int> const& v);
    // however, this is still problematic:
    auto s = random_subrange(std::vector<int>{1,2,3,4});
    //  '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

    • 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 that a span is referring to

    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

    Cheat Sheet

    std::span C++20

    (click for fullscreen view)