Ranges: Composable Algorithms
C++20 ranges revolutionize how we work with sequences, enabling composable, lazy-evaluated operations with cleaner syntax.
Range-Based Algorithms
#include <ranges>
#include <algorithm>
namespace rng = std::ranges;
std::vector<int> vec = {5, 2, 8, 1, 9, 3};
// Old style
std::sort(vec.begin(), vec.end());
// Range style
rng::sort(vec); // Operates on entire range
// With projection
struct Person { std::string name; int age; };
std::vector<Person> people = /*...*/;
rng::sort(people, {}, &Person::age); // Sort by age
Views: Lazy Evaluation
Views are lightweight range adaptors that don’t own data and evaluate lazily:
#include <ranges>
namespace views = std::views;
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// filter: Select elements matching predicate
auto even = vec | views::filter([](int x) { return x % 2 == 0; });
// No computation yet - lazy!
// transform: Apply function to each element
auto squared = vec | views::transform([](int x) { return x * x; });
// take: First n elements
auto first_three = vec | views::take(3);
// drop: Skip first n elements
auto skip_two = vec | views::drop(2);
Composing Views
Views compose using the pipe operator:
auto result = vec
| views::filter([](int x) { return x % 2 == 0; })
| views::transform([](int x) { return x * x; })
| views::take(3);
for (int x : result) {
std::cout << x << ' '; // 4 16 36
}
// Computation happens here during iteration
Common Views
views::iota
Generate sequence of numbers:
// Infinite sequence: 1, 2, 3, ...
for (int i : views::iota(1) | views::take(5)) {
std::cout << i << ' '; // 1 2 3 4 5
}
// Bounded sequence
for (int i : views::iota(1, 6)) {
std::cout << i << ' '; // 1 2 3 4 5
}
views::reverse
std::vector<int> vec = {1, 2, 3, 4, 5};
for (int x : vec | views::reverse) {
std::cout << x << ' '; // 5 4 3 2 1
}
views::keys and views::values
For associative containers:
std::map<std::string, int> map = {{"a", 1}, {"b", 2}, {"c", 3}};
for (const auto& key : map | views::keys) {
std::cout << key << ' '; // a b c
}
for (int value : map | views::values) {
std::cout << value << ' '; // 1 2 3
}
views::split
std::string str = "hello,world,test";
for (auto word : str | views::split(',')) {
for (char c : word) {
std::cout << c;
}
std::cout << '\\n';
}
// hello
// world
// test
views::join
Flatten range of ranges:
std::vector<std::vector<int>> nested = {{1, 2}, {3, 4}, {5, 6}};
for (int x : nested | views::join) {
std::cout << x << ' '; // 1 2 3 4 5 6
}
views::zip (C++23)
Combine multiple ranges:
std::vector<int> nums = {1, 2, 3};
std::vector<std::string> words = {"one", "two", "three"};
for (auto [num, word] : views::zip(nums, words)) {
std::cout << num << ": " << word << '\\n';
}
// 1: one
// 2: two
// 3: three
Materializing Views
Convert views to containers:
auto view = vec
| views::filter([](int x) { return x % 2 == 0; })
| views::transform([](int x) { return x * 2; });
// Convert to vector
std::vector<int> result(view.begin(), view.end());
// Or use ranges::to (C++23)
auto result2 = view | ranges::to<std::vector>();
Owning Views
views::all
Create view of existing range:
std::vector<int> vec = {1, 2, 3, 4, 5};
auto v = views::all(vec);
ranges::ref_view
Non-owning view (reference):
std::vector<int> vec = {1, 2, 3};
ranges::ref_view view(vec);
ranges::owning_view
Takes ownership of range:
auto owned = ranges::owning_view(std::vector{1, 2, 3});
Range Adaptors
Filtering and Transforming
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Filter odd numbers, square them, take first 3
auto result = numbers
| views::filter([](int x) { return x % 2 == 1; })
| views::transform([](int x) { return x * x; })
| views::take(3);
// Result: 1, 9, 25
Reverse and Drop
auto result = numbers
| views::reverse
| views::drop(2)
| views::take(3);
// Start from end, skip 2, take 3: 8, 7, 6
Projection Support
Range algorithms accept projections:
struct Person {
std::string name;
int age;
};
std::vector<Person> people = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35}
};
// Find person with age > 30
auto it = rng::find_if(people, [](int age) { return age > 30; }, &Person::age);
// Sort by name
rng::sort(people, {}, &Person::name);
Practical Examples
Processing Text Lines
std::string text = "line1\\nline2\\nline3\\nline4";
for (auto line : text | views::split('\\n') | views::take(2)) {
for (char c : line) std::cout << c;
std::cout << '\\n';
}
Generating Fibonacci Sequence
auto fibonacci = views::iota(0)
| views::transform([](int n) {
// ... compute nth fibonacci
})
| views::take(10);
Conceptual Check
What is the key advantage of ranges views over eager evaluation?
Runtime Environment
Interactive Lab
1#include <iostream>
2#include <vector>
3#include <ranges>
4
5namespace views = std::views;
6
7int main() {
8 std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
9
10 // Compose operations
11 auto result = nums
12 | views::filter([](int x) { return x % 2 == 0; })
13 | views::transform([](int x) { return x * x; })
14 | views::take(3);
15
16 std::cout << "Even squares (first 3): ";
17 for (int x : result) {
18 std::cout << x << ' ';
19 }
20 std::cout << '\n';
21
22 // iota with reverse
23 std::cout << "Countdown: ";
24 for (int x : views::iota(1, 6) | views::reverse) {
25 std::cout << x << ' ';
26 }
27 std::cout << '\n';
28
29 return 0;
30}
System Console
Waiting for signal...