trainings/AdvancedCppV2/Presentation/moder_cpp_cpp20_ranges.md

14 KiB
Raw Permalink Blame History

Ranges

Let's start with simple example:

int main() {
    auto const ints = {0, 1, 2, 3, 4, 5};
    auto even = [](int i) { return 0 == i % 2; };
    auto square = [](int i) { return i * i; };

    for (int i : ints | std::views::filter(even) | std::views::transform(square)) {
        std::cout << i << ' ';
    }
}
0 4 16

New iterators

  • std::ranges::input_range: specifies a range whose iterator type satisfies input_iterator (can iterate from begin to end at least once)
  • std::ranges::output_range: specifies a range whose iterator type satisfies output_iterator
  • std::ranges::forward_range: specifies a range whose iterator type satisfies forward_iterator (can iterate from begin to end more than once)
  • std::ranges::bidirectional_range: specifies a range whose iterator type satisfies bidirectional_iterator (can iterate forward and backward more than once)
  • std::ranges::random_access_range: specifies a range whose iterator type satisfies random_access_iterator (can jump in constant time to an arbitrary element with the index operator [])
  • std::ranges::contiguous_range: specifies a range whose iterator type satisfies contiguous_iterator (elements are stored consecutively in memory)

Improvements for iterators

Whenever we iterate through value, we need to check if we reach the end. But We can provide an optimization, which avoids comparing it != end. We can do this by passing std::unreachable_sentinel. This sentinel always returns false when compared, that's why the compiler can optimize the process. but be careful if you provide wrong input, like searching numbers which don't exist in acontainer you got Segmentation fault.

int main() {
    std::vector<int> vec = { 1, 2, 3, 4, 5, 6, 7, 8, 9};
    std::ranges::shuffle(vec, std::mt19937(std::random_device()()));
    std::cout << *std::ranges::find(vec.begin(), std::unreachable_sentinel, 5) << '\n';
    // change this for 10 and you got Segmentation fault
}

see unreachable.sentinel


More improvements - predicates

Whenever you wanted to sort structure based on the class member you need to provide a special comparator:

struct Student {
    int index_;
    std::string name_;
    double average_;
};

int main() {
    std::vector<Student> students{{.index_ = 123456, .name_ = "Jordan", .average_ = 4.53},
                                  {.index_ = 654321, .name_ = "Michael", .average_ = 4.51},
                                  {.index_ = 246892, .name_ = "John", .average_ = 4.56},
                                  {.index_ = 743561, .name_ = "Jane", .average_ = 4.44},
                                  {.index_ = 811111, .name_ = "Anna", .average_ = 4.46}};

    std::sort(begin(students), end(students), [](const auto& lhs, const auto& rhs){
       return lhs.average_ < rhs.average_;
    });

    for (const auto& [index, name, average] : students) {
        std::cout << "student: " << name << " | index: " << index << " | avergae: " << average << '\n';
    }
    /*  student: Jane | index: 743561 | avergae: 4.44
        student: Anna | index: 811111 | avergae: 4.46
        student: Michael | index: 654321 | avergae: 4.51
        student: Jordan | index: 123456 | avergae: 4.53
        student: John | index: 246892 | avergae: 4.56 */
}

Now you can write it much faster!:

struct Student {
    int index_;
    std::string name_;
    double average_;
};

int main() {
    std::vector<Student> students{{.index_ = 123456, .name_ = "Jordan", .average_ = 4.53},
                                  {.index_ = 654321, .name_ = "Michael", .average_ = 4.51},
                                  {.index_ = 246892, .name_ = "John", .average_ = 4.56},
                                  {.index_ = 743561, .name_ = "Jane", .average_ = 4.44},
                                  {.index_ = 811111, .name_ = "Anna", .average_ = 4.46}};

    // container, comparator, projection
    std::ranges::sort(students, {}, &Student::average_);

    for (const auto& [index, name, average] : students) {
        std::cout << "student: " << name << " | index: " << index << " | avergae: " << average << '\n';
    }
}

All magic is done by the new parameter projection. It points to address to member and use it to provide a comparison: std::invoke(comp, std::invoke(proj, *(it + n)), std::invoke(proj, *it))


View

Here is a quote from Eric Nieblers range-v3 implementation which is the base for the C++20 ranges: "Views are composable adaptations of ranges where the adaptation happens lazily as the view is iterated." In other words: view is not an owner of range. So we don't need to copy/move elements, only perform some action. Only exception is a std::views::single which owns the single element it is viewing.

Let's use Foo which prints whenever we create/ copy/ move or delete him.

int main() {
    auto even = [](const auto& el) { return !(el.id() % 2); };
    std::vector<Foo> vec {Foo{1}, Foo{2}, Foo{3}, Foo{4}};
 
    std::cout << "Start algorithm\n"; 
    for (const auto& foo : vec 
        | std::views::filter(even) 
        | std::views::drop_while([](const auto& el){ return el.id() < 4; })) {     
        std::cout << "foo: " << foo << '\n';
    }
}

We don't make any copy!

C'tor id: 1
C'tor id: 2
C'tor id: 3
C'tor id: 4
Copy C'tor id: 1
Copy C'tor id: 2
Copy C'tor id: 3
Copy C'tor id: 4        
D'tor id:4
D'tor id:3
D'tor id:2
D'tor id:1
Start algorithm
foo: 4
D'tor id:1
D'tor id:2
D'tor id:3
D'tor id:4
___

View (2)

This is also usefull to create range loop which iterate reversed:

int main() {
    std::vector<Student> students{{.index_ = 123456, .name_ = "Jordan", .average_ = 4.53},
                                  {.index_ = 654321, .name_ = "Michael", .average_ = 4.51},
                                  {.index_ = 246892, .name_ = "John", .average_ = 4.56},
                                  {.index_ = 743561, .name_ = "Jane", .average_ = 4.44},
                                  {.index_ = 811111, .name_ = "Anna", .average_ = 4.46}};

    std::ranges::sort(students, {}, &Student::average_);

    for (const auto& [index, name, average] : std::views::reverse(students)) {
        std::cout << "student: " << name << " | index: " << index << " | avergae: " << average << '\n';
    }
    /*
    student: John | index: 246892 | avergae: 4.56
    student: Jordan | index: 123456 | avergae: 4.53
    student: Michael | index: 654321 | avergae: 4.51
    student: Anna | index: 811111 | avergae: 4.46
    student: Jane | index: 743561 | avergae: 4.44
    */
}

View (3)

We can also create range loop which iterates only through the first/last k elements:

int main() {
    std::vector<Student> students{{.index_ = 123456, .name_ = "Jordan", .average_ = 4.53},
                                  {.index_ = 654321, .name_ = "Michael", .average_ = 4.51},
                                  {.index_ = 246892, .name_ = "John", .average_ = 4.56},
                                  {.index_ = 743561, .name_ = "Jane", .average_ = 4.44},
                                  {.index_ = 811111, .name_ = "Anna", .average_ = 4.46}};

    std::ranges::sort(students, {}, &Student::average_);

    for (const auto& [index, name, average] : std::views::reverse(students) | std::views::drop(3)) {
        std::cout << "student: " << name << " | index: " << index << " | avergae: " << average << '\n';
    }
    /*  student: Anna | index: 811111 | avergae: 4.46
        student: Jane | index: 743561 | avergae: 4.44 */

    for (const auto& [index, name, average] : std::views::reverse(students) | std::views::take(3)) {
        std::cout << "student: " << name << " | index: " << index << " | avergae: " << average << '\n';
    }
    /*  student: John | index: 246892 | avergae: 4.56
        student: Jordan | index: 123456 | avergae: 4.53
        student: Michael | index: 654321 | avergae: 4.51 */
}

std::map

int main() {
    std::map<int, std::string> map{{1, "One"}, {2, "Two"}, {3, "Three"}, {4, "Four"}};
    auto odd = [](const auto& el) { return el % 2; };
 
    for (auto el : std::views::keys(map) | std::views::filter(odd)) {
        std::cout << el << ' ';
    }
    std::cout << '\n';
    
    for (const char c : map | std::views::values | std::views::join) {
        std::cout << c << ' ';
    }
}
1 3 
O n e T w o T h r e e F o u r

C++23 and further

Ranges library based on Ranges V3. Unfortunately, in C++20 there is a lack of useful things like creating cycles or zip functions. In C++23 there will be a few more operations like zip or join_with:

int main() {
    auto x = std::vector{1, 2, 3, 4};
    auto y = std::list<std::string>{"α", "β", "γ", "δ", "ε"};
    auto z = std::array{'A', 'B', 'C', 'D', 'E', 'F'};
 
    /*  1 α A
        2 β B
        3 γ C
        4 δ D */
    for (const auto& [num, grec, alpha] : std::views::zip(x, y, z)) {
        std::cout << num << ' ' << grec << ' ' << alpha << '\n';
    }

    std::map<int, std::string> map{{1, "One"}, {2, "Two"}, {3, "Three"}, {4, "Four"}};
    auto ends_with_e = [](const auto& el) { return el.back() == 'e'; };
    const auto joined = std::views::values(map) | std::views::filter(ends_with_e) | std::views::join_with(' ');
    /* Two Four */
    for (const auto& el : joined) {
        std::cout << el;
    }
}

actions (C++23)

There is a proposal to extend ranges in C++23 to allow easy convert range to container (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p1206r6.pdf)

int main() {
    std::map<int, std::string> map{
        {1, "Hello"}, {2, "Abcd"}, {3, "Hello"}, {4, "Aaa"}, {5, "Ala"}, {6, "Abcd"}};

    // Since C++23
    for (const auto& el : map 
                        | std::views::values 
                        | std::ranges::to<std::vector> // Since C++23
                        | std::ranges::sort
                        | std::ranges::unique) {
        std::cout << el << '\n';
    }
}
Aaa
Abcd
Ala
Hello

actions (C++26)

In C+26 we should get also operator|=.

Before C++26
int main() {
    std::vector<std::string> vec{"Hello", "Abcd", "Hello", "Aaa", "Ala", "Abcd"};

    std::ranges::sort(vec);
    auto ret = std::ranges::unique(vec);
    vec.erase(ret.begin(), ret.end());
}
In C++26
int main() {
    std::vector<std::string> vec{"Hello","Abcd", "Hello", "Aaa", "Ala", "Abcd"};
    vec |= std::ranges::sort | std::ranges::unique;
}
___

Exercise 1

Open project searcher and implement function searchFiles which returns all files containing keyWord.

Possible output:

"C:\\Users\\mateusz\\Documents\\Nokia2022\\Basic\\Course_part1\\exercises\\searcher/files\\fileA.hpp"
"C:\\Users\\mateusz\\Documents\\Nokia2022\\Basic\\Course_part1\\exercises\\searcher/files\\fileC.hpp"

Exercise 2

Open project students:

  • implement function filterStudents,
  • Student should be disqualified whether his average is below minAvergae,
  • Write an algorithm that prints students using ranges (do not implement friend operator<<)

Output:

Name: Jane | index: 743561 | average: 4.44
Name: Tom | index: 811111 | average: 4.36
Name: Mike | index: 811111 | average: 4.45