Power Range(r)s – C++20





5.00/5 (3 votes)
About C++20 Ranges library
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
#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
Someone with great responsibility
Comes 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:
#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:
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:
// ...
auto range = ints | std::views::filter(even) | std::views::transform(inc);
// ...
Another option to call the opposite function:
// ...
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:
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 firstn
elements from a range.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 returnsfalse
.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 firstn
elements from the range.std::views::drop_while(range, pred)
– Drop the first elements untilpred
returnsfalse
.std::views::split(range, pattern)
– Split the range into several ranges wheneverpattern
exists on the range.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: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 rangestd::views::keys(key_value_range)
– Takes all the keys from a key-value consisting viewstd::views::values(key_value_range)
– Takes all the values from a key-value consisting view: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:
#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:
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:
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:
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