Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++20

Power Range(r)s – C++20

5.00/5 (3 votes)
22 Apr 2023CPOL3 min read 5.8K  
About C++20 Ranges library
C++20 Ranges library - what are the ranges, how to use them, what mistakes should we avoid and how to apply ranges on custom collections.

The new feature C++20 provided us is partially here and can help us with our primary mission as C++ developers: Maintain the code and protect it from strangers! So let’s protect the world! **code, I meant the code.

What is a Range?

Range is a concept definition (std::ranges::range) that defines a collection which supports begin and end functionality of iterators (as any STL collection).

The range library brings us the ability to pipe functions which iterate over the rage elements and to evaluate specific algorithms on each one of them and even modify them.

Usage Example

C++
#include <iostream>
#include <vector>
#include <ranges>

int main() {
    std::vector<int> ints = {0, 1, 2, 3, 4, 5};

    auto even = [](int i) { return i % 2 == 0; };
    auto square = [](int i) { return i * i; };
    
    for (int i : ints | std::views::filter(even) | std::views::transform(square)) {
    // Equivalent to:
    // for (int i : std::views::transform(std::views::filter(ints, even), square)) {
    // for (int i : std::ranges::transform_view
    //     {std::ranges::filter_view{ints, even}, square}) {
        std::cout << i << ' ';
    } // Prints: 0 4 16

    return EXIST_SUCCESS;
}

Explanation

std::views::filter and std::views::transform are “Range adaptors”. Range adaptors can take a viewable_range or a view using the pipe operator as well as a regular parameter.

std::ranges::viewable_range – A concept which defines a range that can be safely converted into a view.

std::ranges::view – A concept which defines a range type that has constant time copy, move, and assignment operations (e.g., a pair of iterators or a generator Range that creates its elements on-demand. Notably, the standard library containers are ranges, but not views.)
* In order to dive deeper into the requirements, please refer to cppreference – view.

With Great Power
Comes Great
Responsibility.

Someone with great responsibility

Although this step takes C++ toward other languages standards such as Bash of Linux and AngularJs, it should be treated carefully. Any missing thought when using ranges abilities might bring our worst enemies into taking down our code performances.

Consider the following case:

C++
#include <iostream>
#include <vector>
#include <ranges>

int main() {
    std::vector<int> ints = {1, 3, 5, 7, 9, 11};

    auto even = [](auto elem) { return elem % 2 == 0; };
    auto inc = [](auto elem) { return ++elem; };
    
    auto range = ints | std::views::transform(inc) | std::views::filter(even);
    for (int i : range) { std::cout << i << ' '; } // Prints: 2 4 6 8 10 12

    return EXIST_SUCCESS;
}

On line 11, we can see that the total number of operations that we performed is 2n, and on this example, this is the optimal solution. But let’s consider the following case:

C++
std::vector<int> ints = {1, 3, 5, 7, 9, 11};
auto odd = [](auto elem) { return elem % 2; };
auto inc = [](auto elem) { return ++elem; };
auto range = ints | std::views::transform(inc) | std::views::filter(odd);
for (int i : range) { std::cout << i << ' '; } // Prints:

Here, we got again 2n operations, although all we really needed was n operations, and to archive that, all we needed to do is to use the opposite filter before the transformation:

C++
// ...
auto range = ints | std::views::filter(even) | std::views::transform(inc);
// ...

Another option to call the opposite function:

C++
// ...
auto notf = [](auto &func) {
    return [&](auto elem) {
        return !func(elem);
    };
};
auto range = ints | std::views::filter(notf(odd)) | std::views::transform(inc);
// ...

Parentheses & Adaptors

There is no meaning to parentheses between adaptors calls. Unlike mathematical expression, here on functions call, we won’t be able to change the calling order based on parentheses. For example:

C++
auto inc = [](auto elem) { return ++elem; };
auto square = [](auto elem) { return elem * elem; };
auto div = [](auto elem) { return elem / 2; };
auto range = ints | std::views::transform(inc) | 
             std::views::transform(square) | std::views::transform(div);
// auto range = ints | std::views::transform(inc) | 
// (std::views::transform(square) | std::views::transform(div)); // Same as above
for (int i : range) { std::cout << i << ' '; }

Additional Views

  • std::views::take(range, n) – Take the first n elements from a range.
    C++
    auto range = collection | std::views::take(3);
  • std::views::take_while(range, pred) – Take all the elements from the beginning of the range, until the pred returns false.
    C++
    std::vector<int> ints = {1, 3, 5, 7, 2, 4, 9, 11};
    auto range = ints | std::views::take_while([](auto elem) 
                        { return elem < 7; }); // Returns: {1, 3, 5}
  • std::views::drop(range, n) – Drop the first n elements from the range.
  • std::views::drop_while(range, pred) – Drop the first elements until pred returns false.
  • std::views::split(range, pattern) – Split the range into several ranges whenever pattern exists on the range.
    C++
    std::string str = "Hello,World,of,C++,ranges";
    auto ranges = str | std::views::split(',');
    for (auto range : ranges) {
        for (auto elem : range) {
            std::cout << elem;
        }
        std::cout << '\n';
    }

    Result

    Hello
    World
    of
    C++
    ranges
  • std::views::join(ranges) – Join split ranges:
    C++
    std::string str = "Hello,World,of,C++,ranges";
    auto ranges = str | std::views::split(',');
    auto join_ranges = ranges | std::views::join;
    for (auto elem : join_ranges) {
        std::cout << elem << " ";
    } // Prints: H e l l o W o r l d o f C + + r a n g e s
  • std::views::reverse(range) – Reverse elements order in range
  • std::views::keys(key_value_range) – Takes all the keys from a key-value consisting view
  • std::views::values(key_value_range) – Takes all the values from a key-value consisting view:
    C++
    std::map<std::string, int> my_map;
    my_map.insert({"key1", 2});
    my_map.insert({"key2", 4});
    my_map.insert({"key3", 5});
    my_map.insert({"key4", 6});
    my_map.insert({"key5", 8});
    std::cout << "Keys: ";
    for (auto key : my_map | std::views::keys) {
        std::cout << key << " ";
    }
    
    std::cout << "\nValues: ";
    for (auto value : my_map | std::views::values) {
        std::cout << value << " ";
    }
    // Prints:
    // Keys: key1 key2 key3 key4 key5 
    // Values: 2 4 5 6 8 

For more available view, see cppreference – ranges.

Custom Ranges Collection

For this section, I’ll use the custom collection I described in a previous article: Maintain Your Iterations – Iterators Customization – Part 3.

Assume we have a custom made collection, which supports iterators:

C++
#include <iostream>
#include <vector>
#include <ranges>
#include "custom_iterator.h" // Taken from a previous article: 
//https://cppsenioreas.wordpress.com/2020/10/04/maintain-your-iterations-iterators-customization-part-3/

/* Reminder - my_item:
template <HasMul T>
class my_item {
public:
    my_item(const T &a, const T &b) : value_a(a), value_b (b) {}

    T item() { return value_a * value_b; }
    void set_a(T a) { value_a = a; }
    void set_b(T b) { value_b = b; }

private:
    T value_a, value_b;
};
*/

int main() {
    my_item_collection<std::vector, int> custom_ints2;
    custom_ints2.add_item(5, 3);
    custom_ints2.add_item(2, 7);
    custom_ints2.add_item(1, 4);

    return EXIT_SUCCESS;
}

We can now iterate over our custom collection using ranges adaptors:

C++
int main() {
// ... Define custom collection ...
    auto my_item_even = [] <typename T> 
         (my_item<T> &elem){ return elem.item() % 2 == 0; };
    auto my_item_square = [] <typename T> (my_item<T> &elem) 
                             {return elem.item() * elem.item(); };

    for (auto elem : custom_ints2 | std::views::filter(my_item_even) | 
         std::views::transform(my_item_square)) {
        std::cout << elem << ' ';
    } // Prints: 196 16
    std::cout << '\n';
// ...
}

However, here we created custom access methods just to implement methodology that already exists in our code:

C++
auto even = [](auto elem) { return elem % 2 == 0; };
auto square = [](auto elem) { return elem * elem; };

To avoid creation of custom methods that implement an existing methodology, we can use a single custom conversion method:

C++
auto my_item_to_int = [] <typename T> (my_item<T> &elem){ return elem.item(); };

for (auto elem : custom_ints2 | std::views::transform(my_item_to_int) | 
     std::views::filter(even) | std::views::transform(square)) {
    std::cout << elem << ' ';
}
std::cout << '\n';

Summary

C++ 20 Ranges is a feature that makes our code easier to maintain in the long term and can significantly reduce the loops amount on our programs.

Full examples repository: cppsenioreas-ranges

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)