The C++20 Ranges library (<ranges>) extends the STL with a composable, lazy pipeline model for sequence operations. Range views are lightweight, lazy wrappers that transform, filter, and project sequences without copying data. They compose with the pipe operator |: data | views::filter(pred) | views::transform(fn) | views::take(n) applies filter, then transform, then take — evaluated lazily element-by-element only when iterated. Ranges also upgrade STL algorithms to accept single range arguments instead of iterator pairs.
Introduction
The classic STL algorithm model requires iterator pairs: std::sort(v.begin(), v.end()), std::find_if(v.begin(), v.end(), pred). This works, but composing multiple operations becomes verbose. To filter a vector, then transform each element, then take the first ten results, you would either write three separate passes (creating intermediate vectors), or craft a complex hand-rolled loop.
C++20’s Ranges library solves this with two innovations. First, range-based algorithms accept containers directly — std::ranges::sort(v) instead of std::ranges::sort(v.begin(), v.end()). They also support projections: std::ranges::sort(v, {}, &Person::name) sorts by the name field without a custom comparator.
Second, and more fundamentally, range views are lazy, composable transformations. A view describes what to do with elements but does no work until you iterate. Views compose with | into pipelines. v | views::filter(isEven) | views::transform(square) | views::take(5) reads from left to right like a sentence — “take v, keep the even elements, square each one, stop after 5” — with no intermediate allocations.
This article teaches the Ranges library from the ground up: range concepts, constrained algorithms, views and their lazy semantics, the pipe operator, the standard view library, and practical patterns for data processing pipelines.
Range Concepts: What Is a Range?
A range is anything with begin() and end() that return iterators. std::vector, std::list, std::string, C arrays, and custom types all qualify. C++20 formalizes this as a Concept:
#include <iostream>
#include <ranges>
#include <vector>
#include <list>
#include <array>
#include <string>
using namespace std;
// Demonstrating what qualifies as a range
template<ranges::range R>
void printRange(const R& r) {
cout << "[ ";
for (const auto& elem : r) cout << elem << " ";
cout << "]\n";
}
int main() {
// All of these are ranges:
vector<int> v = {1, 2, 3, 4, 5};
array<int, 4> a = {10, 20, 30, 40};
list<int> l = {100, 200, 300};
string s = "hello";
int arr[] = {7, 8, 9};
printRange(v);
printRange(a);
printRange(l);
printRange(s);
printRange(arr);
// Range categories (concepts):
cout << "\n--- Range category checks ---\n";
// random_access_range: can jump to any element in O(1)
cout << "vector is random_access_range: "
<< ranges::random_access_range<vector<int>> << "\n";
// bidirectional_range: can iterate forward and backward
cout << "list is bidirectional_range: "
<< ranges::bidirectional_range<list<int>> << "\n";
cout << "list is random_access_range: "
<< ranges::random_access_range<list<int>> << "\n";
// contiguous_range: elements are contiguous in memory (like array)
cout << "vector is contiguous_range: "
<< ranges::contiguous_range<vector<int>> << "\n";
cout << "list is contiguous_range: "
<< ranges::contiguous_range<list<int>> << "\n";
// sized_range: know the size in O(1)
cout << "vector is sized_range: "
<< ranges::sized_range<vector<int>> << "\n";
// Range size and empty
cout << "\nSize of v: " << ranges::size(v) << "\n";
cout << "Empty v: " << ranges::empty(v) << "\n";
vector<int> empty_v;
cout << "Empty empty_v: " << ranges::empty(empty_v) << "\n";
return 0;
}
Output:
[ 1 2 3 4 5 ]
[ 10 20 30 40 ]
[ 100 200 300 ]
[ h e l l o ]
[ 7 8 9 ]
--- Range category checks ---
vector is random_access_range: 1
list is bidirectional_range: 1
list is random_access_range: 0
vector is contiguous_range: 1
list is contiguous_range: 0
vector is sized_range: 1
Size of v: 5
Empty v: 0
Empty empty_v: 1
Step-by-step explanation:
ranges::range<R>is a Concept that any type withbegin()/end()satisfies. TheprintRangetemplate is constrained to only accept ranges — a cleaner constraint than unconstrainedtypename T.- Range categories form a hierarchy:
contiguous_range⊂random_access_range⊂bidirectional_range⊂forward_range⊂input_range. Avectorsatisfies all; alistsatisfies only up tobidirectional_rangebecause you cannot jump to the Nth element in O(1). ranges::size(r)andranges::empty(r)work uniformly on any sized or forward range. Unlike.size()and.empty()(member functions), these are free functions that work on C arrays too.- The category determines which operations are available: binary search requires
random_access_range;reverserequiresbidirectional_range;findrequires onlyinput_range.
Constrained Algorithms: ranges:: vs std::
C++20 adds ranges:: versions of every standard algorithm that accept ranges instead of iterator pairs and support projections:
#include <iostream>
#include <ranges>
#include <algorithm>
#include <vector>
#include <string>
using namespace std;
struct Person {
string name;
int age;
double salary;
};
void printPeople(const vector<Person>& people, const string& label) {
cout << label << ":\n";
for (const auto& [name, age, salary] : people) {
cout << " " << name << " (age " << age << ", $" << (int)salary << ")\n";
}
}
int main() {
vector<Person> people = {
{"Charlie", 35, 85000},
{"Alice", 28, 95000},
{"Bob", 42, 72000},
{"Diana", 31, 110000},
{"Eve", 28, 88000}
};
// --- Sorting with projection ---
cout << "--- Sort by name ---\n";
vector<Person> byName = people;
ranges::sort(byName, {}, &Person::name); // Project to name field
printPeople(byName, "Sorted by name");
cout << "\n--- Sort by salary descending ---\n";
vector<Person> bySalary = people;
ranges::sort(bySalary, greater<double>{}, &Person::salary);
printPeople(bySalary, "Sorted by salary (desc)");
cout << "\n--- Sort by age then name ---\n";
vector<Person> byAgeName = people;
ranges::sort(byAgeName, [](const Person& a, const Person& b) {
if (a.age != b.age) return a.age < b.age;
return a.name < b.name;
});
printPeople(byAgeName, "Sorted by age then name");
// --- find_if with projection ---
cout << "\n--- Find by projection ---\n";
auto it = ranges::find(people, "Bob", &Person::name);
if (it != people.end()) {
cout << "Found: " << it->name << " age=" << it->age << "\n";
}
// --- count_if ---
int youngCount = ranges::count_if(people, [](const Person& p) {
return p.age < 35;
});
cout << "People under 35: " << youngCount << "\n";
// --- any_of, all_of, none_of ---
bool allAdults = ranges::all_of(people, [](const Person& p) {
return p.age >= 18;
});
bool anyHighEarner = ranges::any_of(people, [](const Person& p) {
return p.salary > 100000;
});
cout << "All adults: " << allAdults << "\n";
cout << "Any high earner: " << anyHighEarner << "\n";
// --- min_element / max_element with projection ---
auto youngest = ranges::min_element(people, {}, &Person::age);
auto richest = ranges::max_element(people, {}, &Person::salary);
cout << "Youngest: " << youngest->name << " (age " << youngest->age << ")\n";
cout << "Richest: " << richest->name << " ($" << (int)richest->salary << ")\n";
// --- copy_if ---
vector<Person> seniors;
ranges::copy_if(people, back_inserter(seniors), [](const Person& p) {
return p.age >= 35;
});
cout << "\nSeniors (35+):\n";
for (const auto& [name, age, _] : seniors) {
cout << " " << name << " (age " << age << ")\n";
}
// --- transform with projection ---
vector<string> names;
ranges::transform(people, back_inserter(names), &Person::name);
cout << "\nAll names: ";
for (const auto& n : names) cout << n << " ";
cout << "\n";
// --- Comparison: old vs new style ---
vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
// Old style:
sort(v.begin(), v.end());
// New style (single argument, more concise):
ranges::sort(v);
cout << "\nSorted: ";
for (int x : v) cout << x << " ";
cout << "\n";
return 0;
}
Output:
--- Sort by name ---
Sorted by name:
Alice (age 28, $95000)
Bob (age 42, $72000)
Charlie (age 35, $85000)
Diana (age 31, $110000)
Eve (age 28, $88000)
--- Sort by salary descending ---
Sorted by salary (desc):
Diana (age 31, $110000)
Alice (age 28, $95000)
Eve (age 28, $88000)
Charlie (age 35, $85000)
Bob (age 42, $72000)
--- Sort by age then name ---
Alice (age 28, $95000)
Eve (age 28, $88000)
Diana (age 31, $110000)
Charlie (age 35, $85000)
Bob (age 42, $72000)
--- Find by projection ---
Found: Bob age=42
People under 35: 3
All adults: 1
Any high earner: 1
Youngest: Alice (age 28)
Richest: Diana ($110000)
Seniors (35+):
Charlie (age 35)
Bob (age 42)
All names: Charlie Alice Bob Diana Eve
Sorted: 1 1 2 3 4 5 6 9
Step-by-step explanation:
ranges::sort(byName, {}, &Person::name)takes three arguments: the range, a comparator (default{}=less<>), and a projection (a callable or member pointer applied to each element before comparison). The projection&Person::nameextracts the name from eachPersonfor comparison.- Projections eliminate the need for custom comparators in many cases.
ranges::min_element(people, {}, &Person::age)finds the person with the minimum age — no lambda needed, just a member pointer. ranges::sort(v)(single-argument form) is identical tosort(v.begin(), v.end()). The range overloads are not separate functions — they are the same function with different overloads, using the unified range protocol.ranges::find(people, "Bob", &Person::name)projects eachPersonto its name, then finds the element where the projected value equals"Bob". Without projection, you’d writefind_if(people.begin(), people.end(), [](const Person& p) { return p.name == "Bob"; }).- The
{}inranges::sort(byName, {}, &Person::name)is a default-constructed comparator —less<>{}. You can passgreater<>{}for descending sort or any callable.
Views: Lazy, Composable Transformations
Views are the core innovation of the Ranges library. A view is a range that applies a transformation lazily — it does no work until elements are accessed during iteration.
#include <iostream>
#include <ranges>
#include <vector>
#include <string>
#include <numeric>
using namespace std;
namespace views = std::views; // Alias for brevity
int main() {
vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// --- views::filter ---
cout << "=== filter ===\n";
auto evens = data | views::filter([](int n) { return n % 2 == 0; });
for (int n : evens) cout << n << " ";
cout << "\n";
// --- views::transform ---
cout << "=== transform ===\n";
auto squares = data | views::transform([](int n) { return n * n; });
for (int n : squares) cout << n << " ";
cout << "\n";
// --- Composed pipeline ---
cout << "=== filter | transform ===\n";
auto evenSquares = data
| views::filter([](int n) { return n % 2 == 0; })
| views::transform([](int n) { return n * n; });
for (int n : evenSquares) cout << n << " ";
cout << "\n"; // 4 16 36 64 100
// --- views::take and views::drop ---
cout << "\n=== take and drop ===\n";
auto first3 = data | views::take(3);
cout << "First 3: ";
for (int n : first3) cout << n << " ";
cout << "\n";
auto skip3 = data | views::drop(3);
cout << "After 3: ";
for (int n : skip3) cout << n << " ";
cout << "\n";
// --- views::take_while and views::drop_while ---
auto lessThan5 = data | views::take_while([](int n) { return n < 5; });
cout << "While < 5: ";
for (int n : lessThan5) cout << n << " ";
cout << "\n";
// --- views::reverse ---
cout << "\n=== reverse ===\n";
auto rev = data | views::reverse;
for (int n : rev) cout << n << " ";
cout << "\n";
// --- views::keys and views::values (for maps) ---
cout << "\n=== keys and values ===\n";
map<string, int> scores = {{"Alice", 95}, {"Bob", 72}, {"Carol", 88}};
cout << "Keys: ";
for (const auto& k : scores | views::keys) cout << k << " ";
cout << "\n";
cout << "Values: ";
for (int v : scores | views::values) cout << v << " ";
cout << "\n";
// --- views::enumerate (C++23, but show the iota workaround) ---
cout << "\n=== iota (generate sequence) ===\n";
for (int n : views::iota(1, 11)) cout << n << " ";
cout << "\n";
// Generate squares of 1..10 without a source vector
auto squaresOf10 = views::iota(1, 11)
| views::transform([](int n) { return n * n; });
cout << "Squares 1..10: ";
for (int n : squaresOf10) cout << n << " ";
cout << "\n";
// --- views::join (flatten) ---
cout << "\n=== join ===\n";
vector<vector<int>> nested = {{1, 2, 3}, {4, 5}, {6, 7, 8, 9}};
auto flat = nested | views::join;
cout << "Flattened: ";
for (int n : flat) cout << n << " ";
cout << "\n";
// --- views::split ---
cout << "\n=== split ===\n";
string csv = "alpha,beta,gamma,delta";
for (auto word : csv | views::split(',')) {
string_view sv(word.begin(), word.end());
cout << sv << " ";
}
cout << "\n";
// --- views::zip (C++23) workaround with iota ---
cout << "\n=== zip-like with iota + transform ===\n";
vector<string> names = {"Alice", "Bob", "Carol"};
vector<int> ranks = {1, 2, 3};
for (int i : views::iota(0, (int)names.size())) {
cout << ranks[i] << ". " << names[i] << "\n";
}
return 0;
}
Output:
=== filter ===
2 4 6 8 10
=== transform ===
1 4 9 16 25 36 49 64 81 100
=== filter | transform ===
4 16 36 64 100
=== take and drop ===
First 3: 1 2 3
After 3: 4 5 6 7 8 9 10
While < 5: 1 2 3 4
=== reverse ===
10 9 8 7 6 5 4 3 2 1
=== keys and values ===
Keys: Alice Bob Carol
Values: 72 88 95
=== iota (generate sequence) ===
1 2 3 4 5 6 7 8 9 10
Squares 1..10: 1 4 9 16 25 36 49 64 81 100
=== join ===
Flattened: 1 2 3 4 5 6 7 8 9
=== split ===
alpha beta gamma delta
=== zip-like with iota + transform ===
1. Alice
2. Bob
3. Carol
Step-by-step explanation:
data | views::filter(pred)does not iteratedataor allocate anything. It returns a view object that, when iterated, skips elements wherepredreturns false. Every element is evaluated lazily — one at a time, as the range-for loop requests them.- The pipe
|operator composes views.data | views::filter(pred) | views::transform(fn)creates a pipeline view. When iterated: fetch next element fromdata, applyfilter, if it passes applytransform, yield the result. No intermediate container is created. views::iota(1, 11)generates the sequence{1, 2, 3, ..., 10}on demand — no storage required. Composing it withtransformcreates an infinite-or-finite generator pipeline with zero allocations.views::joinflattens a range-of-ranges into a single range.nested | views::joiniterates all inner vectors sequentially without copying.views::split(',')splits a string (or any range) by a delimiter, producing a view of sub-ranges. The sub-ranges are string views into the original — no allocation. Converting tostring_viewgives a lightweight view for printing.
Lazy Evaluation: Why It Matters
The key property of views is laziness — they do no work until elements are pulled from them:
#include <iostream>
#include <ranges>
#include <vector>
using namespace std;
namespace views = std::views;
int main() {
// Demonstrate laziness: only processes elements actually consumed
int filterCount = 0;
int transformCount = 0;
vector<int> data(1'000'000); // 1 million elements
iota(data.begin(), data.end(), 1); // Fill with 1..1000000
// Lazy pipeline: filter evens, square them, take only first 5
auto pipeline = data
| views::filter([&](int n) {
++filterCount;
return n % 2 == 0;
})
| views::transform([&](int n) {
++transformCount;
return n * n;
})
| views::take(5);
cout << "Pipeline created — no work done yet\n";
cout << "filterCount: " << filterCount << "\n"; // 0
cout << "transformCount: " << transformCount << "\n"; // 0
cout << "\nIterating pipeline:\n";
for (int n : pipeline) {
cout << n << " ";
}
cout << "\n";
cout << "\nAfter iteration of first 5 results:\n";
cout << "filterCount: " << filterCount << "\n"; // ~10 (enough to find 5 evens)
cout << "transformCount: " << transformCount << "\n"; // 5
// Compare: eager approach
int eagerFilter = 0, eagerTransform = 0;
vector<int> step1; // Eager filter
for (int n : data) {
++eagerFilter;
if (n % 2 == 0) step1.push_back(n);
}
vector<int> step2; // Eager transform
for (int n : step1) {
++eagerTransform;
step2.push_back(n * n);
}
vector<int> step3(step2.begin(), step2.begin() + 5); // Eager take
cout << "\nEager approach to get same 5 results:\n";
cout << "eagerFilter: " << eagerFilter << "\n"; // 1000000
cout << "eagerTransform: " << eagerTransform << "\n"; // 500000
cout << "Result: ";
for (int n : step3) cout << n << " ";
cout << "\n";
return 0;
}
Output:
Pipeline created — no work done yet
filterCount: 0
transformCount: 0
Iterating pipeline:
4 16 36 64 100
After iteration of first 5 results:
filterCount: 10
transformCount: 5
Eager approach to get same 5 results:
eagerFilter: 1000000
eagerTransform: 500000
Result: 4 16 36 64 100
Step-by-step explanation:
- Creating the pipeline does zero work —
filterCountandtransformCountare both0after setup. The view objects are just descriptions of what to do, not executions. - When iterated, the pipeline processes only as many elements as needed: to find 5 even numbers, we only need to look at the first 10 elements (1, 2, 3, 4, 5, 6, 7, 8, 9, 10 — five odds filtered out, five evens found). The remaining 999,990 elements are never touched.
- The eager approach processes the entire 1,000,000 elements through
filterand then 500,000 throughtransform— even though we only needed 5 results. This is 200,000x more work than the lazy pipeline. views::take(5)is the “short-circuit” — it signals the pipeline to stop after 5 results are produced. Without it, the lazy pipeline would process all elements too (just without intermediate allocations).- Laziness also composes with infinite ranges:
views::iota(1) | views::filter(isPrime) | views::take(10)generates the first 10 prime numbers from an infinite sequence — impossible with eager evaluation.
A Complete Data Processing Pipeline
#include <iostream>
#include <ranges>
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
#include <map>
using namespace std;
namespace views = std::views;
struct Employee {
string name;
string department;
int yearsExp;
double salary;
bool remote;
};
int main() {
vector<Employee> employees = {
{"Alice", "Engineering", 8, 125000, true},
{"Bob", "Marketing", 3, 72000, false},
{"Carol", "Engineering", 5, 98000, true},
{"Dave", "HR", 2, 65000, false},
{"Eve", "Engineering", 12, 148000, true},
{"Frank", "Marketing", 7, 88000, false},
{"Grace", "Engineering", 1, 75000, false},
{"Henry", "HR", 9, 95000, true},
{"Iris", "Engineering", 4, 92000, true},
{"Jack", "Marketing", 6, 82000, true}
};
// --- Query 1: Senior remote engineers, sorted by salary ---
cout << "=== Senior remote engineers (5+ years) ===\n";
auto seniorRemote = employees
| views::filter([](const Employee& e) {
return e.department == "Engineering"
&& e.yearsExp >= 5
&& e.remote;
})
| views::transform([](const Employee& e) -> string {
return e.name + " ($" + to_string((int)e.salary) + ")";
});
for (const auto& desc : seniorRemote) {
cout << " " << desc << "\n";
}
// --- Query 2: Top 3 earners across all departments ---
cout << "\n=== Top 3 earners ===\n";
vector<Employee> sorted = employees;
ranges::sort(sorted, greater<double>{}, &Employee::salary);
auto top3 = sorted | views::take(3);
for (const auto& [name, dept, yrs, sal, rem] : top3) {
cout << " " << name << " (" << dept << "): $" << (int)sal << "\n";
}
// --- Query 3: Department salary averages ---
cout << "\n=== Average salary by department ===\n";
map<string, vector<double>> bySalary;
for (const auto& e : employees) {
bySalary[e.department].push_back(e.salary);
}
for (const auto& [dept, salaries] : bySalary) {
double avg = accumulate(salaries.begin(), salaries.end(), 0.0)
/ salaries.size();
cout << " " << dept << ": $" << (int)avg << "\n";
}
// --- Query 4: Names of marketing employees, alphabetically ---
cout << "\n=== Marketing team (alphabetical) ===\n";
auto marketingNames = employees
| views::filter([](const Employee& e) {
return e.department == "Marketing";
})
| views::transform(&Employee::name);
vector<string> mNames(marketingNames.begin(), marketingNames.end());
ranges::sort(mNames);
for (const auto& n : mNames) cout << " " << n << "\n";
// --- Query 5: Count remote workers ---
long remoteCount = ranges::count_if(employees, &Employee::remote);
cout << "\nRemote workers: " << remoteCount << " / " << employees.size() << "\n";
// --- Query 6: Generate employee IDs (iota + zip-like) ---
cout << "\n=== Employee roster with IDs ===\n";
for (auto [idx, emp] : views::iota(1001)
| views::take(employees.size())
| views::transform([&](int id) {
static int i = 0;
return pair{id, ref(employees[i++])};
})) {
cout << " ID-" << idx << ": " << emp.get().name << "\n";
}
// --- Query 7: Salary bands using transform and take_while ---
cout << "\n=== Salary bands ===\n";
auto highEarners = employees
| views::filter([](const Employee& e) { return e.salary >= 100000; })
| views::transform(&Employee::name);
auto midEarners = employees
| views::filter([](const Employee& e) {
return e.salary >= 75000 && e.salary < 100000;
})
| views::transform(&Employee::name);
cout << "High earners ($100k+): ";
for (const auto& n : highEarners) cout << n << " ";
cout << "\nMid earners ($75k-$100k): ";
for (const auto& n : midEarners) cout << n << " ";
cout << "\n";
return 0;
}
Output:
=== Senior remote engineers (5+ years) ===
Carol ($98000)
Eve ($148000)
Iris ($92000)
=== Top 3 earners ===
Eve (Engineering): $148000
Alice (Engineering): $125000
Carol (Engineering): $98000
=== Average salary by department ===
Engineering: $107600
HR: $80000
Marketing: $80666
=== Marketing team (alphabetical) ===
Bob
Frank
Jack
Remote workers: 6 / 10
=== Employee roster with IDs ===
ID-1001: Alice
ID-1002: Bob
...
=== Salary bands ===
High earners ($100k+): Alice Eve
Mid earners ($75k-$100k): Carol Henry Iris
Standard Views Reference
#include <iostream>
#include <ranges>
#include <vector>
#include <string>
using namespace std;
namespace views = std::views;
int main() {
vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// --- views::counted ---
cout << "counted(v.begin(), 4): ";
for (int n : views::counted(v.begin(), 4)) cout << n << " ";
cout << "\n";
// --- views::all ---
// Converts a range to a view (useful for explicit adaptation)
auto allV = views::all(v);
cout << "all size: " << ranges::size(allV) << "\n";
// --- views::common ---
// Makes begin/end the same type (needed for legacy algorithms)
auto filtered = v | views::filter([](int n) { return n > 5; });
auto common = filtered | views::common;
// Now can use with std:: algorithms that require same iterator type:
cout << "sum of >5: "
<< accumulate(common.begin(), common.end(), 0) << "\n";
// --- views::elements (for tuple-like ranges) ---
vector<tuple<int, string, double>> records = {
{1, "Alice", 95.0},
{2, "Bob", 72.0},
{3, "Carol", 88.0}
};
cout << "IDs: ";
for (auto id : records | views::elements<0>) cout << id << " ";
cout << "\n";
cout << "Names: ";
for (const auto& name : records | views::elements<1>) cout << name << " ";
cout << "\n";
cout << "Scores: ";
for (auto score : records | views::elements<2>) cout << score << " ";
cout << "\n";
// --- Materializing a view into a container (C++23: ranges::to) ---
// C++20 way: construct vector from view
auto evenSquares = v
| views::filter([](int n) { return n % 2 == 0; })
| views::transform([](int n) { return n * n; });
vector<int> result(evenSquares.begin(), evenSquares.end());
cout << "Even squares: ";
for (int n : result) cout << n << " ";
cout << "\n";
// --- Passing a view to a ranges:: algorithm ---
auto pipeline = v | views::transform([](int n) { return n * 3; });
auto maxVal = ranges::max_element(pipeline);
cout << "Max of v*3: " << *maxVal << "\n";
return 0;
}
Output:
counted(v.begin(), 4): 1 2 3 4
all size: 10
sum of >5: 40
IDs: 1 2 3
Names: Alice Bob Carol
Scores: 95 72 88
Even squares: 4 16 36 64 100
Max of v*3: 30
Step-by-step explanation:
views::counted(iterator, n)creates a view of exactlynelements starting atiterator— useful when you have an iterator and count but not an end iterator.views::commonadapts a view whosebeginandendreturn different types (common with filter views) to a view where they return the same type. This is required for compatibility with pre-C++20 STL algorithms that expectiterator == sentinel.views::elements<N>projects a range of tuple-like elements to their Nth element — similar toviews::keys(N=0) andviews::values(N=1) but for any tuple position.- Materializing a view into a container uses the range constructor:
vector<T> result(view.begin(), view.end()). C++23 addsranges::to<vector>()for cleaner syntax. - Views compose with
ranges::algorithms —ranges::max_element(pipeline)finds the maximum element of a transformed view without materializing it.
Views Quick Reference
| View | Description | Example |
|---|---|---|
views::filter(pred) |
Keep elements where pred is true | v | views::filter(isEven) |
views::transform(fn) |
Apply fn to 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) |
Elements while pred is true | v | views::take_while(lt10) |
views::drop_while(pred) |
Skip elements while pred is true | v | views::drop_while(lt10) |
views::reverse |
Elements in reverse order | v | views::reverse |
views::keys |
Keys of a pair/map range | m | views::keys |
views::values |
Values of a pair/map range | m | views::values |
views::elements<N> |
Nth element of tuple-like range | v | views::elements<2> |
views::join |
Flatten range of ranges | nested | views::join |
views::split(delim) |
Split range by delimiter | s | views::split(',') |
views::iota(start, end) |
Integer sequence [start, end) | views::iota(1, 11) |
views::iota(start) |
Infinite integer sequence | views::iota(0) |
views::counted(it, n) |
n elements from iterator | views::counted(it, 5) |
views::all(r) |
Convert range to view | views::all(container) |
views::common |
Homogenize iterator types | v | views::common |
Common Mistakes
Mistake 1: Storing a view that references a destroyed container.
auto getView() {
vector<int> v = {1, 2, 3};
return v | views::filter(isEven); // DANGLING: v is destroyed at return
}
auto view = getView();
for (int n : view) cout << n; // UB: iterating dangling view
// Fix: return the materialized vector, not a view of a local
Mistake 2: Modifying a container while iterating a view over it.
vector<int> v = {1, 2, 3, 4, 5};
auto view = v | views::filter(isEven);
v.push_back(6); // May reallocate v — view now dangles
for (int n : view) cout << n; // UB
// Fix: don't modify the source while iterating a view
Mistake 3: Expecting views to be const-safe like containers.
void process(const vector<int>& v) {
// views::filter on a const range — fine, elements are const
for (int n : v | views::filter(isEven)) cout << n << " ";
}
// But: some views are not const-iterable (notably filter_view)
// Use auto& (non-const) when assigning views if you'll iterate them
auto view = v | views::filter(pred); // OK
const auto view2 = v | views::filter(pred);
for (int n : view2) {} // May fail to compile for filter_view
Mistake 4: Using a sentinel-based view with pre-C++20 algorithms.
auto filtered = v | views::filter(pred);
// OLD algorithms expect same type for begin/end:
accumulate(filtered.begin(), filtered.end(), 0); // May not compile
// Fix: use views::common or ranges:: algorithms
accumulate((filtered | views::common).begin(),
(filtered | views::common).end(), 0);
// Better: use ranges::fold_left (C++23) or equivalent
Mistake 5: Assuming views copy data.
auto view = bigVector | views::transform(expensiveFn);
auto view2 = view; // O(1): views are lightweight wrappers, not copies
// But iterating view2 twice calls expensiveFn twice each time
// If you need the results stored, materialize: vector<T> v(view.begin(), view.end())
Conclusion
The C++20 Ranges library transforms how you write sequence operations in C++. Instead of explicit loops, iterator pairs, and intermediate containers, you compose declarative pipelines that read naturally and execute efficiently.
The two pillars of the library work together. ranges:: algorithms accept whole ranges and support projections — ranges::sort(people, {}, &Person::name) replaces a custom comparator with a member pointer. Views compose lazily with | — data | views::filter(pred) | views::transform(fn) | views::take(n) evaluates on demand, processing only the elements needed, with no intermediate allocations.
Laziness is ranges’ most important property. A million-element pipeline that only consumes 5 results processes only those 5 — not the full million. This is what makes ranges genuinely composable: you can chain as many views as needed without paying for the work not done.
Practical benefits are immediate: map iteration with views::keys and views::values, string splitting with views::split, sequence generation with views::iota, flattening with views::join, and window operations with views::take_while. Combined with structured bindings and constrained algorithms, the Ranges library makes C++ data processing code concise, correct, and expressive — closer to what you would write in a functional language, but with C++’s performance characteristics.
C++23 extends the library further with views::zip, views::chunk, views::slide, views::adjacent, ranges::to<Container>, and monadic operations on views. The trajectory is clear: ranges are the future of C++ sequence processing.




