Sequence Views Sequence Views Views
Views Don't Own Resources
An object is said to be an owner of a resource (memory, file handle, connection, thread, lock, …) if it is responsible for its lifetime (initialization/creation, finalization/destruction).
C++17
#include <string_view>
- lightweight (= cheap to copy, can be passed by value)
- non-owning (= not responsible for allocating or deleting memory)
- read-only view (= does not allow modification of target string)
- of a character range or string(-like) object (
std::string
/"literal"
/ …) - primary use case: read-only function parameters (avoids temporary copies)
Avoids Unnecessary Allocations
Motivation: Read-only String Parameters
We don't want/expect additional copies or memory allocations for a read-only parameter!
The traditional choice std::string const&
is problematic:
- A
std::string
can be constructed from string literals or an iterator range to achar
sequence. - If we pass an object as function argument that is not a string itself, but something that can be used to construct a string, e.g., a string literal or an iterator range, a new temporary string object will be allocated and bound to the const reference.
string_view
avoids temporary copies:
#include <vector>
#include <string>
#include <string_view>
void f_cref (std::string const& s) { … }
void f_view (std::string_view s) { … }
int main () {
std::string stdStr = "Standard String";
auto const cStr = "C-String";
std::vector<char> v {'c','h','a','r','s','\0'};
f_cref(stdStr); // no copy
f_cref(cStr); // temp copy
f_cref("Literal"); // temp copy
f_cref({begin(v),end(v)}); // temp copy
f_view(stdStr); // no copy
f_view(cStr); // no copy
f_view("Literal"); // no copy
f_view({begin(v),end(v)}); // no copy
}
Function Parameters
If You… | Use Parameter Type |
---|---|
always need a copy of the input string inside the function | std::string
pass by value |
want read-only access
|
#include <string_view>
std::string_view |
want read-only access
|
std::string const&
pass by const reference |
want the function to modify the input string in-place
(you should try to avoid such output parameters) |
std::string &
pass by (non-const) reference |
See here for more explanations.
Making
With Constructor Calls
#include <string>
#include <string_view>
#include <iostream>
int main () {
std::string s = "Some Text";
std::cout << "s: " << s << '\n';
// view whole string
std::string_view sv1 { s };
std::cout << "sv1: " << sv1 << '\n';
// view subrange
std::string_view sv2 {begin(s)+2, begin(s)+5};
std::cout << "sv2: " << sv2 << '\n';
std::string_view sv3 {begin(s)+2, end(s)};
std::cout << "sv3: " << sv3 << '\n';
}
With Special Literal "…"sv
#include <string>
#include <string_view>
#include <iostream>
int main () {
using namespace std::string_view_literals;
auto literal_view = "C-String Literal"sv;
std::cout << literal_view;
}
Careful: View might outlive string!
std::string_view sv1 {std::string{"Text"}};
cout << sv1; // string object already destroyed!
using namespace std::string_literals;
std::string_view sv2 {"std::string Literal"s};
cout << sv2; // string object already destroyed!
You should use string_view
mainly as function parameter!
Interface
C++20
#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
,std::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) |
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} );
}
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.
Making
As View of Whole Container/Range:…
#include <array>
#include <vector>
#include <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};
// auto-deduce type/length:
std::span sw1 { w }; // span<int>
std::span sa1 { a }; // span<int,4>
fmt::print("sw1: {}\n", sw1);
fmt::print("sa1: {}\n", sa1);
// explicit read-only view:
std::span sw2 { std::as_const(w) };
fmt::print("sw2: {}\n", sw2);
// with explicit type parameter:
std::span<int> sw3 { w };
std::span<int> sa2 { a };
std::span<int const> sw4 { w };
fmt::print("sw3: {}\n", sw3);
fmt::print("sa2: {}\n", sa2);
fmt::print("sw4: {}\n", sw4);
// with explicit type parameter & length:
std::span<int,4> sa3{ a };
fmt::print("sa3: {}\n", sa3);
}
As View of Container Subsequence:…
#include <array>
#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};
std::span s2 {begin(w)+2, end(w)};
fmt::print("s1: {}\n", s1);
fmt::print("s2: {}\n", s2);
}
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);
}
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
Function Parameters
As- decouple function implementations from the data representation / container type
- clearly communicate the intent of only reading/altering elements in a sequence, but not modifying the underlying memory/data structure
- make 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 view's target cannot invalidate the memory that the view refers to during the function's execution (unless it is done in another thread)
views can speed up 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});
- not always clear what object/memory the view refers to
- returned views can be (inadvertently) invalidated
// 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
- easy to produce dangling views, because we have to manually track lifetimes to ensure that no view outlives its target
- even if the memory owner is still alive, it might invalidate the memory that a view is referring to
std::string str1 = "Text";
std::string_view sv {str1};
if ( … ) {
std::string str2 = "Text";
sv = str2;
}
cout << sv; // str2 already destroyed!
std::string_view sv1 {"C-String Literal"};
cout << sv1; //
std::string_view sv2 {std::string{"Text"}};
cout << sv2; // string object already destroyed!
using namespace std::string_literals;
std::string_view sv3 {"std::string Literal"s};
cout << sv3; // string object already 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(w.end(), {6,7,8,9});
cout << s[0]; // w might hold new memory
Comments…