Beginner's Guide
    First Steps
    Input & Output
    Basic Custom Types
    Diagnostics
    Standard Library
    Code Organization
    Powerful Custom Types
    Generic Programming
    Memory Management
    Software Design Basics

    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
    • don't always need a copy
    • are using C++17 / 20
    std::string_view
    want read-only access
    • don't always need a copy
    • are stuck with C++98 / 11 / 14
    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

    Making string_views Making

    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)};
    using namespace std::string_view_literals;
    auto literal_view = "C-String Literal"sv;
    cout << literal_view;
    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::vectorstd::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)

    As Parameter (Primary Use Case) Parameters

    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} );

    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.

    Explicitly Making Spans Making

    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 };
    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 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

    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

    Making Spans From Spans 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();

    Usage Guidelines Guidelines

    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});

    Avoid Returning Views Avoid Returning

    • 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, 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

    Avoid Local View Variables Avoid Variables

    • 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!

    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