C++20 Big Four: Range
14 May 2023This article concludes the “Big Four” series of C++20. Many readers may wonder: what exactly is the “Ranges” feature that places it among the Big Four?
Initially, I had the same question. The first three features in C++20 were “revolutionary”: Modules transformed project organization, Coroutines redefined concurrency, and Concepts brought the biggest change to template programming since its inception. So, what makes Ranges deserving of the Big Four? It fundamentally changes the way we handle loops by providing a higher level of abstraction.
What is a Range?
A Range is defined as any object with begin()
and end()
iterators. There are two main types of Ranges: Containers and Views. Containers own the data pointed to by begin()
and end()
, while Views do not. Views are lightweight and easy to copy and move.
Consider an example to understand the concept of Ranges:
- Given an array of integers, filter out the even numbers.
- Square each remaining number.
- Reverse the order of the squared numbers.
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
const std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto even = [](int i) { return 0 == i % 2; };
auto square = [](int i) { return i * i; };
std::vector<int> temp;
std::copy_if(begin(numbers), end(numbers), std::back_inserter(temp), even);
std::transform(begin(temp), end(temp), begin(temp), square);
std::vector<int> temp2(rbegin(temp), rend(temp));
for (auto iter = begin(temp2); iter!=end(temp2); ++iter)
std::cout << *iter << ' ';
}
Before C++20 Ranges, we had to use copy_if
, rbegin
, and other utilities to perform these operations, often requiring intermediate variables. Let’s see how Ranges simplify this:
int main() {
const std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto even = [](int i) { return 0 == i % 2; };
auto square = [](int i) { return i * i; };
std::ranges::reverse_view rv{
std::ranges::transform_view{
std::ranges::filter_view{ numbers, even }, square
}
};
for (const auto& i : rv)
std::cout << i << ' ';
}
In this example, we use filter_view
to select even numbers, then apply transform_view
to square them, and finally pass the result to reverse_view
to reverse the order.
In terms of readability, some might find the first approach easier since the logical sequence matches the top-down reading order. The second approach requires reading from the inside out to align with the logic. However, the efficiency of the two differs significantly: the first requires intermediate variables like temp
and temp2
, while Ranges and Views avoid these entirely. Views are just references to the data, not copies.
To improve readability, C++20 introduced the pipeline operator |
:
int main() {
const std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto even = [](int i) { return 0 == i % 2; };
auto square = [](int i) { return i * i; };
namespace sv = std::views;
auto result = numbers | sv::filter(even) | sv::transform(square) | sv::reverse;
for (const auto& i : result)
std::cout << i << ' ';
}
With the pipeline operator |
, std::views::filter
can be combined seamlessly, enhancing readability and simplicity. Now we can read the sequence left-to-right: filter for even numbers, square them, then reverse the order. This eliminates intermediate variables and improves readability and code brevity.
Lazy Evaluation
The above examples demonstrate the efficiency of Ranges. Views only reference source data, avoiding copies and reducing memory usage. Another powerful feature is lazy evaluation. Lazy evaluation means that View operations are only executed when needed, not immediately. These operations are queued in a pipeline and are executed only when the View is iterated over, allowing each element to pass through the pipeline to produce a final result.
Standard Library Views
Views | Description |
---|---|
std::views::all |
Returns a view of all elements starting from the first. |
std::views::drop |
Drops a specified number of elements from the beginning and returns a view of the rest. |
std::views::drop_while |
Drops elements until the first that doesn’t match a specified predicate, then returns the remaining view. |
std::views::filter |
Returns a view of all elements satisfying a specified predicate. |
std::views::join |
Flattens a two-dimensional array into a one-dimensional view. |
std::views::join_with |
Flattens a two-dimensional array with specified elements in between. |
std::views::reverse |
Returns a view with elements in reverse order. |
std::views::split |
Splits a view by a specified delimiter and returns the resulting sub-views. |
std::views::take |
Returns a view of the first specified number of elements. |
std::views::take_while |
Returns a view of elements until the first that doesn’t match a specified predicate. |
std::views::transform |
Returns a view with each element transformed by a specified function. |
std::views::keys |
Generates a view of the first elements in a pair-like structure. |
std::views::values |
Generates a view of the second elements in a pair-like structure. |
std::views::elements |
Generates a view of the Nth element in each tuple of a tuple-like structure. |
std::views::zip |
Combines multiple sub-arrays into a two-dimensional view. |
std::views::zip_transform |
Combines multiple sub-arrays using a specified operation (e.g., add). |
std::views::adjacent |
Returns a view of all contiguous N-element combinations. |
std::views::adjacent_transform |
Extends adjacent to allow specified operations on contiguous N elements. |
std::views::stride |
Returns a view that takes every nth element. |
std::views::chunk |
Splits a view into chunks of a specified size. |
std::views::counted |
Returns a view starting at a specific position and taking a specified number of elements. |
std::views::common |
Converts a non-common range into a common range. |
std::views::as_const |
Returns a const view of the elements. |
std::views::as_rvalue |
Returns an rvalue view of the elements. |
References
- Ranges Composition in C++
- Introduction to C++ Ranges
- A Complete Guide to C++20 Ranges
- A Beginner’s Guide to C++ Ranges and Views by Hannes Hauswedell