Sequence ViewsSequence ViewsViews
Reminder: Ownership
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).
std::string_view
string_view
string_view
#include string_view
- lightweight (= cheap to copy, can be passed by value)
- non-owning (= not responsible for allocating or deleting memory)
- read-only view
- of a character range or string(-like) object
(std::string
/"literal"
/std::vector<char>
/ …)
Avoids Unnecessary Memory Allocations Avoids Memory Allocations Avoids Allocations
A std::string
can be constructed from string literals or
an iterator range (to a char
sequence).
We don't want/expect additional copies or memory allocations
for a std::string const&
function parameter.
If we pass an object 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 string copies in these cases:
#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 cStr = "C-String";
std::vector<char> v {'c','h','a','r','s'};
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
}
String-Like Function Parameters String-Like Parameters 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
|
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 |
With Constructor Calls
std::string s = "Some Text";
// view whole string
std::string_view sv1 { s };
// view subrange
std::string_view sv2 {begin(s)+2, 5};
std::string_view sv3 {begin(s)+2, end(s)};
With Special Literal "…"sv
using namespace std::string_view_literals;
auto literal_view = "C-String Literal"sv;
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 / only as function parameter!
std::span
span
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
,std::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 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:…
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:…
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.
As View of Whole Container/Range:…
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>
// explicit read-only view:
std::span sw2 { std::as_const(w) };
// with explicit type parameter:
std::span<int> sw3 { w };
std::span<int> sa2 { a };
std::span<int const> sw4 { w };
// with explicit type parameter & length:
std::span<int,4> sa3{ a };
As View of Container Subsequence:…
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)};
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));
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
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();
Views As Function Parameters Views As Parameters As Parameters
- decouple function implementations from the data represenation / 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 views's target cannot invalidate the memory that the view 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 views
- not always clear what object/memory the view 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, t.his 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 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 which 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!
std::span s;
if ( … ) {
std::vector<int> w {1,2,3,4,5};
s = std::span(w);
} // w destroyed!
cout << s[0]; // 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.push_back({6,7,8,9});
cout << s[0]; // w might hold new memory