In the C++20 standard library, the Ranges library introduces a novel abstraction for manipulating sequences of elements. Two fundamental concepts within this library are ranges and views.
Range
A range is an abstraction that represents a sequence of elements that can be traversed. Conceptually, it encapsulates the notions of “beginning” and “end” into a unified entity. Traditionally, C++ iteration involves pairs of iterators, such as [begin, end). The Ranges library simplifies this process by introducing the std::ranges::range concept, which merges these iterators into a more cohesive structure.
- A range can be thought of as “something you can iterate over.”
- For instance,
std::vector<int>
is a range. A built-in arrayint arr[10]
can be adapted to a range. Many STL containers can seamlessly act as ranges.
View
A view is a specialized type of range. It is a lightweight, composable, and non-owning adaptor that transforms or filters another range. Views do not store the entire set of elements; instead, they typically refer to another underlying range and apply a transformation “on-the-fly” during iteration. This property of views makes them often lazy: they only compute values as you iterate through them.
auto result = some_range
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });
Putting It All Together
The introduction of ranges and views enables a more functional, pipeline-oriented programming style using standard C++. Instead of manually writing loops and constructing temporary vectors, these abstractions can be directly connected. For instance:
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6};
auto even_squares = vec
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });
for (int x : even_squares) {
std::cout << x << " ";
}
}
This snippet demonstrates the ease of chaining transformations and filters together using views. The vec represents a range, and even_squares is a view pipeline that lazily filters and transforms vec’s elements as we traverse it.
Filtering and Transforming a std::vector
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {10, 1, 42, -5, 0, 7, 2};
// Filter out negative numbers and zeros, then double the remaining ones.
auto positive_doubled = numbers
| std::views::filter([](int x) { return x > 0; })
| std::views::transform([](int x) { return x * 2; });
for (int val : positive_doubled) {
std::cout << val << " ";
}
return 0;
}
Using std::array
with take
and reverse
Views
#include <iostream>
#include <array>
#include <ranges>
int main() {
std::array<int, 5> arr = {5, 10, 15, 20, 25};
// Take the first 3 elements and then reverse them
auto first_three_reversed = arr
| std::views::take(3)
| std::views::reverse;
for (int val : first_three_reversed) {
std::cout << val << " ";
}
return 0;
}
Using transform
on a std::string
to Change Case
#include <iostream>
#include <string>
#include <ranges>
#include <cctype>
int main() {
std::string text = "Hello, Ranges!";
// Transform each character to uppercase
auto upper_view = text
| std::views::transform([](unsigned char c) { return static_cast<char>(std::toupper(c)); });
for (char c : upper_view) {
std::cout << c;
}
return 0;
}
Key Takeaways:
- Views are lightweight, non-owning adapters that apply transformations or filters lazily as you iterate.
- They can be chained together with the
|
(pipe) operator, making code more readable and expressive. - They work on any range, including STL containers (
std::vector
,std::array
,std::deque
,std::string
) and special views likeiota
. - They encourage a functional programming style in C++, reducing the need for manual loops, temporary containers, and complex iterator manipulations.