std::span
/ gsl::span
std::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) |
Span vs. C-Style (Pointer,Length)
void foo(span<int> s); |
void foo(int* a, int n); |
|
|
|
|
|
|
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);
Call With Container/Range:…
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; C++17
print_chars( sv );
Call With Iterator Range:…
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} );
Call With Stack-Array:…
int a[100];
print_ints( a );
Call With Poiner & Length:…
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
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); C++20
Comparing
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.
View Container
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 Container Subsequence View Subsequence
std::vector<int> w {0, 1, 2, 3, 4, 5, 6};
|----.------'
std::span s1 {begin(w)+2, 5}; C++20
std::span s2 {begin(w)+2, end(w)};
gsl::span<int> s3 {begin(w)+2, 5}; GSL
gsl::span<int> s4 {begin(w)+2, end(w)};
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
Careful!
Span Might Outlive Memory
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!
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
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});
- 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
- 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!