C++ Ranges & Views: filter, transform, Pipelines & Lazy Evaluation (C++20) Complete Masterclass
Welcome to Phase 21 (Chapter 21): C++ Ranges & Views (C++20) Masterclass! The Ranges library is the biggest quality-of-life improvement in C++ since C++11. Ranges eliminate iterator pairs from algorithm calls, views enable lazy composable pipelines with the | operator, and the whole system is zero-cost due to lazy evaluation and compile-time composition.
A range is any type with begin() and end(). The std::ranges namespace provides range-based versions of all STL algorithms that accept a single range instead of a begin/end pair โ eliminating a whole class of bugs.
Old STL vs Ranges:
Old: std::sort(v.begin(), v.end()); โ requires begin/end pair, can be mismatched
New: std::ranges::sort(v); โ accepts the range directly, impossible to mismatch
Old: std::sort(v.begin(), v.end(), pred);
New: std::ranges::sort(v, pred); โ cleaner, composable with projections
#include <iostream>
#include <vector>
#include <algorithm>
#include <ranges>
#include <string>
struct Person {
std::string name;
int age;
double salary;
};
int main() {
std::vector<int> nums{5, 2, 8, 1, 9, 3, 7, 4, 6};
// Ranges algorithms โ no begin/end, range-based projections!
std::ranges::sort(nums);
std::cout << "sorted: ";
for (int n : nums) std::cout << n << " ";
std::cout << "
";
std::ranges::reverse(nums);
std::cout << "reversed: ";
for (int n : nums) std::cout << n << " ";
std::cout << "
";
// find with predicate
auto it = std::ranges::find(nums, 7);
if (it != nums.end())
std::cout << "7 at index: " << std::distance(nums.begin(), it) << "
";
// count_if
int evens = std::ranges::count_if(nums, [](int n){ return n%2==0; });
std::cout << "evens: " << evens << "
";
// โโโ Projections โ apply transform before comparison โโโโโโโโโโโโโโโโโโ
std::vector<Person> people{
{"Charlie", 35, 90000}, {"Alice", 28, 75000},
{"Bob", 42, 120000}, {"Diana", 31, 95000}
};
// Sort by age using projection (much cleaner than lambda comparator!)
std::ranges::sort(people, {}, &Person::age);
std::cout << "
Sorted by age:
";
for (const auto& p : people)
std::cout << " " << p.name << " (" << p.age << ")
";
// Sort by name length using projection
std::ranges::sort(people, std::ranges::less{}, [](const Person& p){ return p.name.size(); });
std::cout << "
Sorted by name length:
";
for (const auto& p : people) std::cout << " " << p.name << "
";
// Find max salary using projection
auto richest = std::ranges::max_element(people, {}, &Person::salary);
std::cout << "
Highest salary: " << richest->name << " ($" << richest->salary << ")
";
return 0;
}
Views are lazy, non-owning range adapters. They compute elements on-demand โ no intermediate containers, no copies. Multiple views are chained with the pipe | operator into composable pipelines.
#include <iostream>
#include <ranges>
#include <vector>
#include <string>
#include <algorithm>
int main() {
std::vector<int> numbers{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// โโโ Basic views โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// filter โ keep only matching elements
auto evens = numbers | std::views::filter([](int n){ return n%2==0; });
std::cout << "evens: ";
for (int n : evens) std::cout << n << " ";
std::cout << "
";
// transform โ map each element
auto squares = numbers | std::views::transform([](int n){ return n*n; });
std::cout << "squares: ";
for (int n : squares) std::cout << n << " ";
std::cout << "
";
// take โ first N elements
auto first5 = numbers | std::views::take(5);
std::cout << "take(5): ";
for (int n : first5) std::cout << n << " ";
std::cout << "
";
// drop โ skip first N elements
auto after3 = numbers | std::views::drop(3);
std::cout << "drop(3): ";
for (int n : after3) std::cout << n << " ";
std::cout << "
";
// take_while / drop_while
auto lessThan6 = numbers | std::views::take_while([](int n){ return n<6; });
std::cout << "take_while(<6): ";
for (int n : lessThan6) std::cout << n << " ";
std::cout << "
";
// reverse
auto reversed = numbers | std::views::reverse;
std::cout << "reversed: ";
for (int n : reversed) std::cout << n << " ";
std::cout << "
";
// โโโ Composed pipeline โ chaining multiple views โโโโโโโโโโโโโโโโโโโโโโโ
auto pipeline = numbers
| std::views::filter([](int n){ return n%2==0; }) // keep evens
| std::views::transform([](int n){ return n*n; }) // square them
| std::views::take(4); // take first 4
std::cout << "even squares (first 4): ";
for (int n : pipeline) std::cout << n << " "; // 4 16 36 64
std::cout << "
";
// โโโ iota โ generate integer sequence โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Finite: [0, 10)
std::cout << "iota(0,10): ";
for (int n : std::views::iota(0, 10)) std::cout << n << " ";
std::cout << "
";
// Infinite iota + take (lazy โ only computes what's needed!)
auto firstNFibs = std::views::iota(1)
| std::views::take(8)
| std::views::transform([](int n){ return n*n; });
std::cout << "1^2 to 8^2: ";
for (int n : firstNFibs) std::cout << n << " ";
std::cout << "
";
// โโโ keys and values views on map โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#include <map>
std::map<std::string, int> scores{{"Alice",95},{"Bob",87},{"Charlie",92}};
std::cout << "keys: ";
for (const auto& k : scores | std::views::keys) std::cout << k << " ";
std::cout << "
";
std::cout << "values: ";
for (auto v : scores | std::views::values) std::cout << v << " ";
std::cout << "
";
// โโโ String views โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
std::vector<std::string> words{"hello","ranges","are","amazing","cpp20","is","great"};
auto longUpperWords = words
| std::views::filter([](const std::string& s){ return s.length() > 3; })
| std::views::transform([](const std::string& s){
std::string upper = s;
std::ranges::transform(upper, upper.begin(), ::toupper);
return upper;
});
std::cout << "Long words uppercase: ";
for (const auto& w : longUpperWords) std::cout << w << " ";
std::cout << "
";
return 0;
}
#include <iostream>
#include <ranges>
#include <vector>
#include <string>
#include <algorithm>
int main() {
std::vector<int> numbers{1,2,3,4,5,6,7,8,9,10};
// Materialize into vector using ranges::to (C++23)
// In C++20: use ranges::copy + back_inserter
auto evens = numbers | std::views::filter([](int n){ return n%2==0; });
std::vector<int> evenVec;
std::ranges::copy(evens, std::back_inserter(evenVec));
std::cout << "Materialized evens: ";
for (int n : evenVec) std::cout << n << " ";
std::cout << "
";
// Collect with transform into string
std::string result;
for (int n : numbers | std::views::filter([](int n){ return n>5; }))
result += std::to_string(n) + " ";
std::cout << "Greater than 5: " << result << "
";
// ranges::for_each with projection
std::vector<std::string> names{"charlie","alice","bob","diana"};
std::ranges::for_each(names, [](const std::string& n){
std::cout << n << " ";
});
std::cout << "
";
// ranges::transform with back_inserter
std::vector<std::string> upperNames;
std::ranges::transform(names, std::back_inserter(upperNames),
[](std::string s) {
std::ranges::transform(s, s.begin(), ::toupper);
return s;
});
std::cout << "Uppercase: ";
for (const auto& n : upperNames) std::cout << n << " ";
std::cout << "
";
// Check with ranges predicates
bool allPositive = std::ranges::all_of(numbers, [](int n){ return n>0; });
bool anyOver8 = std::ranges::any_of(numbers, [](int n){ return n>8; });
bool noneNeg = std::ranges::none_of(numbers, [](int n){ return n<0; });
std::cout << std::boolalpha
<< "all positive: " << allPositive
<< " any>8: " << anyOver8
<< " none negative: " << noneNeg << "
";
return 0;
}
| View Adaptor | Description | Example |
|---|---|---|
views::filter(pred) | Keep elements matching predicate | v | views::filter(isEven) |
views::transform(fn) | Map each element | v | views::transform(square) |
views::take(n) | First n elements | v | views::take(5) |
views::drop(n) | Skip first n elements | v | views::drop(3) |
views::take_while(pred) | Take while predicate holds | v | views::take_while(lt10) |
views::drop_while(pred) | Skip while predicate holds | v | views::drop_while(lt5) |
views::reverse | Reverse iteration order | v | views::reverse |
views::iota(a, b) | Sequence [a, b) | views::iota(1, 100) |
views::iota(a) | Infinite sequence from a | views::iota(0) | views::take(10) |
views::keys | Keys of pair/map range | myMap | views::keys |
views::values | Values of pair/map range | myMap | views::values |
views::elements<N> | Nth element of tuple range | v | views::elements<2> |
views::zip(r1, r2) | Zip two ranges (C++23) | views::zip(names, scores) |
views::enumerate | Index + element pairs (C++23) | v | views::enumerate |
views::join | Flatten nested ranges | vv | views::join |
views::split(delim) | Split by delimiter | str | views::split('/') |
Q1: Are views lazy or eager?
Views are completely lazy. No computation happens when you create a view pipeline. Elements are computed one-by-one on demand as you iterate. This means views::iota(0) | views::filter(pred) | views::take(5) only evaluates until 5 elements are found โ even from an infinite range.
Q2: Do views own their data?
No โ views are non-owning. They hold references/iterators into the original range. The original container must outlive the view. Never store a view to a local container that goes out of scope โ dangling reference!
Q3: What is a projection in ranges algorithms?
Projections let you specify a transformation to apply to elements before comparison: ranges::sort(people, {}, &Person::age) sorts by age without writing a comparator lambda. Works with member pointers and lambdas.
Q4: Can I collect a view into a container?
In C++20: std::ranges::copy(view, std::back_inserter(vec));. In C++23: auto vec = view | std::ranges::to<std::vector>(); โ much more concise. The to<> function materializes the lazy view into the target container.
Q5: What is the difference between std::ranges::sort and std::sort?
std::ranges::sort(v) accepts a single range โ impossible to pass mismatched iterators. std::sort(v.begin(), v.end()) requires an iterator pair. Both are O(n log n). Ranges version supports projections. Both have the same underlying algorithm.