Article by Ayman Alheraki in September 25 2024 09:28 AM
The C++20 standard introduced many features that modernize and simplify the language, one of the most powerful being Ranges. Ranges offer a cleaner, more readable way of handling algorithms and collections, enhancing the traditional iterator-based approach. In this article, we’ll explore the benefits of C++20 Ranges, their added value over standard ranges (or iterators), and how to use them with examples.
In traditional C++, working with collections (like std::vector
, std::list
, etc.) typically requires iterators. You'd use iterator pairs to define the start and end of a range for algorithms. For example:
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// Traditional C++: using iterators to apply std::for_each
std::for_each(vec.begin(), vec.end(), [](int& n){ n *= 2; });
}
With C++20 Ranges, we can now operate directly on the container itself, eliminating the need for iterator pairs and making the code cleaner and easier to read:
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// C++20 Ranges: using ranges to apply std::ranges::for_each
std::ranges::for_each(vec, [](int& n){ n *= 2; });
}
Improved Code Readability and Simplicity
One of the most immediate benefits of C++20 Ranges is their ability to improve code readability. With traditional iterators, you often have to deal with pairs of begin()
and end()
iterators, which can clutter the code, especially in more complex algorithms. Ranges remove this noise, leading to more intuitive and succinct code.
Example (Traditional Iterators):
std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = std::find(vec.begin(), vec.end(), 3);
Example (C++20 Ranges):
auto it = std::ranges::find(vec, 3);
As shown, the begin()
and end()
calls are no longer needed, leading to a more compact and easier-to-read version of the same functionality.
Range Adaptors for Powerful Composition C++20 introduces range adaptors, which allow you to create pipelines of operations on ranges in a declarative style. This means that instead of mutating a range through multiple algorithms in sequence, you can chain these operations together.
Example using range adaptors:
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8};
// Using ranges to filter and transform a sequence in a single line
auto result = vec
| std::views::filter([](int n){ return n % 2 == 0; }) // filter even numbers
| std::views::transform([](int n){ return n * n; }); // square them
// Display the result
for (int n : result) {
std::cout << n << " "; // Output: 4 16 36 64
}
}
In this example, we filter the vector to select only even numbers, then transform (square) them. This declarative and functional style is significantly more readable and avoids the need for intermediate collections or temporary variables.
Lazy Evaluation
One of the most exciting aspects of C++20 Ranges is their lazy evaluation. When chaining operations like filter
or transform
, the operations are not executed until the results are actually needed. This allows you to work with potentially infinite ranges and avoid unnecessary computations.
Example:
auto infinite = std::views::iota(1)
| std::views::filter([](int n){ return n % 2 == 0; })
| std::views::transform([](int n){ return n * n; });
// Print the first 5 squared even numbers
for (auto it = infinite.begin(); it != infinite.begin() + 5; ++it) {
std::cout << *it << " "; // Output: 4 16 36 64 100
}
Here, iota(1)
generates an infinite sequence starting from 1, but thanks to lazy evaluation, only the first 5 values are computed when needed.
Safer and More Flexible Algorithms Ranges in C++20 offer enhanced safety. Many range-based algorithms can be used without explicitly needing to manage iterator boundaries, reducing the risk of errors such as off-by-one bugs. Moreover, range views are non-owning, meaning they don’t store the underlying data but just operate on the original collection.
Example:
std::vector<int> vec = {1, 2, 3, 4};
// Traditional approach using iterators
auto isSorted = std::is_sorted(vec.begin(), vec.end());
// C++20 Ranges approach
auto isSortedRanges = std::ranges::is_sorted(vec);
Not only is the range-based version shorter, but it also avoids potential iterator mismatch errors, making it safer.
Views vs. Containers Views are a key part of C++20 Ranges and represent a "window" into a container. Views are lightweight, non-owning adaptors that don’t store data but operate on existing ranges. You can apply transformations without modifying the original data, which leads to more memory-efficient operations.
Example:
auto view = std::views::iota(1, 10) | std::views::filter([](int x) { return x % 2 == 0; });
for (int n : view) {
std::cout << n << " "; // Output: 2 4 6 8
}
Here, the iota
view creates a range of numbers from 1 to 9, and the filter
view produces only the even ones, without duplicating the data.
Let's demonstrate a more practical example that showcases the power of C++20 Ranges in combination with std::views
.
int main() {
std::vector<int> numbers = {1, 4, 2, 5, 3, 7, 8, 6};
// Create a pipeline that sorts, filters, and transforms the data
auto processed = numbers
| std::views::sort // Sort the numbers
| std::views::filter([](int n){ return n % 2 == 0; }) // Filter even numbers
| std::views::transform([](int n){ return n * 2; }); // Double the remaining numbers
// Print the results
for (const auto& n : processed) {
std::cout << n << " "; // Output: 4 8 12 16
}
}
This code sorts a list of numbers, filters the even ones, and doubles them, all in a single, easy-to-read pipeline.
C++20 Ranges bring a significant evolution to the way we process collections and sequences. They add readability, composability, and efficiency through their declarative style, range adaptors, and lazy evaluation. By reducing the need for explicit iterator manipulation, they also enhance safety and minimize potential errors.
If you're looking to write more expressive, clean, and efficient C++ code, Ranges are a game-changer. The transition from standard ranges to C++20 Ranges provides an upgrade that will make your code both easier to maintain and more efficient in its execution.