std::span
/ gsl::span
std::span
/ gsl::span
std::span
#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 vs. C-Style (Pointer,Length)
void foo (span<int> s); |
void foo (int* a, int n); |
|
|
|
|
|
|
Using
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);
Call With Container/Range:…
#include <array>
#include <vector>
#include <span>
#include <iostream>
void print_ints (std::span<int const> s) {
for (auto i : s) std::cout << i << ' ';
std::cout << '\n';
}
void print_chars (std::span<char const> s) {
for (auto i : s) std::cout << i;
std::cout << '\n';
}
int main () {
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 );
}
Call With Iterator Range:…
#include <vector>
#include <span>
#include <iostream>
void print_ints (std::span<int const> s) {
for (auto i : s) std::cout << i << ' ';
std::cout << '\n';
}
int main () {
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} );
}
Call With Stack-Array:…
#include <span>
#include <iostream>
void print_ints (std::span<int const> s) {
for (auto i : s) std::cout << i << ' ';
std::cout << '\n';
}
int main () {
int a[20];
…
for (int i = 0; i < 20; ++i) a[i] = i;
print_ints( a );
}
Call With Pointer & Length:…
#include <span>
#include <vector>
#include <iostream>
void print_ints (std::span<int const> s) {
for (auto i : s) std::cout << i << ' ';
std::cout << '\n';
}
// we are going to pretend that this is some memory
// that is managed by an external library
std::vector<int> v {1,2,3,4,5,6,7,8};
// and that these are functions from such a library
size_t get_number_of_elements() { return v.size(); }
int* get_pointer_to_memory() { return v.data(); }
int main () {
// 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
#include <vector>
#include <span>
#include <iostream>
int main () {
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
#include <algorithm> // std::ranges::equal
#include <vector>
#include <span>
#include <fmt/ranges.h> // fmt::print
int main () {
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
fmt::print("memory_same: {}\n", memory_same);
fmt::print("values_same: {}\n", values_same);
}
Making
In C++20, std::span
's template parameters can
be deduced from the constructor arguments.
View Container
#include <vector>
#include <array>
#include <span>
#include <gsl/span>
#include <fmt/ranges.h> // fmt::print
int main () {
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 };
}
View
Subsequence#include <vector>
#include <span>
#include <fmt/ranges.h> // fmt::print
int main () {
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)};
fmt::print("s1: {}\n", s1);
fmt::print("s2: {}\n", s2);
}
View C-Array On Stack
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
View C-Array On Heap
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
Spans From Spans
#include <vector>
#include <span>
#include <fmt/ranges.h> // fmt::print
int main () {
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);
fmt::print("first3elements: {}\n", first3elements);
fmt::print("last3elements: {}\n", last3elements);
size_t offset = 2;
size_t count = 4;
auto subs = s.subspan(offset, count);
fmt::print("subs: {}\n", subs);
}
Guidelines
Primary Use Case: As 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)
- 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:
int foo (std::span<int const> in) { … }
std::vector<int> v {…};
// v will always outlive parameter 'in'!
foo(v);
foo({begin(v), 5});
Careful When 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> 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
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
Example: Memory Invalidation By Owner
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
Comments…