Ranges Library — std::ranges, Views, Pipe Operator, Range Adaptors
In this tutorial, you will learn about Ranges Library. We cover key concepts, practical examples, and best practices to help you master this topic.
C++20 ranges library reimagines STL algorithms and iterators with composable views, lazy evaluation, and a pipe operator (|) that chains transformations into readable data processing pipelines.
What You'll Learn
You will use std::ranges::sort and other constrained algorithms, create lazy views with std::views::filter, transform, take, drop, and reverse, compose views with the pipe operator (|), write custom range adaptors, and understand the difference between views and actions.
Why It Matters
Traditional STL algorithms are powerful but verbose: you must pass begin() and end() every time, and composing multiple operations requires intermediate containers. Ranges solve both problems with single-object range arguments and composable views that evaluate lazily, avoiding unnecessary copies and temporary allocations.
Learning Path
graph LR
A["39: Numeric Algorithms"] --> B["40: Ranges Library"]
B --> C["41: Iterator Types"]
C --> D["42: Function Templates"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
Ranges Algorithms
#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>
int main() {
std::vector<int> v = {5, 2, 8, 1, 9, 3, 7, 4, 6};
// Traditional: std::sort(v.begin(), v.end());
// Ranges: just the container
std::ranges::sort(v);
// Find
auto it = std::ranges::find(v, 5);
if (it != v.end()) {
std::cout << "Found: " << *it << "\n";
}
// Count
auto count = std::ranges::count_if(v, [](int x) { return x > 5; });
std::cout << "Elements > 5: " << count << "\n";
// Projections
struct Person { std::string name; int age; };
std::vector<Person> people = {{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}};
// Sort by age using projection (no custom comparator needed)
std::ranges::sort(people, std::less{}, &Person::age);
for (const auto& p : people) {
std::cout << p.name << " (" << p.age << ") ";
}
std::cout << "\n";
}
Basic Views
Views are lightweight, non-owning, lazy-evaluated wrappers over ranges.
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// filter: select elements matching predicate
auto evens = v | std::views::filter([](int x) { return x % 2 == 0; });
// transform: apply function to each element
auto doubled = v | std::views::transform([](int x) { return x * 2; });
// take: first n elements
auto first3 = v | std::views::take(3);
// drop: skip first n elements
auto after5 = v | std::views::drop(5);
// reverse (requires bidirectional range)
auto reversed = v | std::views::reverse;
// Chaining views (lazy! no intermediate allocations)
auto pipeline = v
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * 10; })
| std::views::take(3);
std::cout << "Pipeline result: ";
for (int x : pipeline) {
std::cout << x << " ";
}
std::cout << "\n"; // 20 40 60
}
Lazy Evaluation
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
int callCount = 0;
auto view = v | std::views::transform([&callCount](int x) {
++callCount;
return x * 2;
});
// No transform has been called yet (lazy)
std::cout << "After view creation, callCount = " << callCount << "\n"; // 0
// First element access triggers one transform
auto it = view.begin();
std::cout << "First element: " << *it << "\n"; // 2
std::cout << "After first access, callCount = " << callCount << "\n"; // 1
// Full iteration triggers the rest
for (int x : view) {
// x is computed on-the-fly
}
std::cout << "After full iteration, callCount = " << callCount << "\n"; // 5
}
Common Views
#include <iostream>
#include <vector>
#include <ranges>
#include <string>
int main() {
// std::views::iota: generate infinite range
auto numbers = std::views::iota(1) | std::views::take(10);
for (int x : numbers) std::cout << x << " ";
std::cout << "\n"; // 1 2 3 4 5 6 7 8 9 10
// std::views::all: create a view over a container
std::vector<int> v = {10, 20, 30};
auto all = std::views::all(v);
// std::views::keys and values (for pair-like ranges)
std::vector<std::pair<int, std::string>> pairs = {{1, "one"}, {2, "two"}};
auto keys = pairs | std::views::keys;
auto vals = pairs | std::views::values;
// std::views::transform with state
int offset = 5;
auto shifted = v | std::views::transform([offset](int x) { return x + offset; });
// std::views::join: flatten nested ranges
std::vector<std::vector<int>> nested = {{1, 2}, {3, 4, 5}, {6}};
auto flat = nested | std::views::join;
for (int x : flat) std::cout << x << " ";
std::cout << "\n"; // 1 2 3 4 5 6
// Split on delimiter (C++20)
std::string text = "hello,world,cpp";
// for (auto word : text | std::views::split(',')) { ... }
}
Views vs Actions
Views are lazy and non-owning. Actions are eager and mutate the container.
#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>
int main() {
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3};
// View: no modification, lazy
auto filtered = v | std::views::filter([](int x) { return x > 3; });
// Action: modify in-place (proposed for C++23/26, not yet in standard)
// Currently use member functions or algorithms
std::ranges::sort(v); // this is an action (mutates v)
// To materialize a view into a container:
std::vector<int> result;
std::ranges::copy(v | std::views::take(5), std::back_inserter(result));
}
Custom Range Adaptor
#include <iostream>
#include <ranges>
#include <vector>
#include <cmath>
// A view that takes every nth element
auto stride(size_t n) {
return std::views::enumerate
| std::views::filter([n](const auto& pair) {
return std::get<0>(pair) % n == 0;
})
| std::views::transform([](const auto& pair) {
return std::get<1>(pair);
});
}
// Or more directly:
namespace detail {
struct StrideFn {
size_t n;
constexpr auto operator()(auto&& range) const {
return std::forward<decltype(range)>(range)
| std::views::drop(0) // placeholder
;
}
};
}
int main() {
auto numbers = std::views::iota(1, 21);
auto everyThird = numbers
| std::views::enumerate
| std::views::filter([](const auto& pair) {
return pair.first % 3 == 0;
})
| std::views::transform([](const auto& pair) {
return pair.second;
});
std::cout << "Every 3rd: ";
for (int x : everyThird) std::cout << x << " ";
std::cout << "\n";
}
Common Mistakes
Mistake 1: Storing a View to a Temporary
auto getView() {
std::vector<int> v = {1, 2, 3};
return v | std::views::filter([](int x) { return x > 1; });
} // Oops: view refers to destroyed vector
Mistake 2: Modifying Container Through View
std::vector<int> v = {1, 2, 3};
for (int& x : v | std::views::filter(...)) {
x *= 2; // may invalidate view iterators if filter predicate changes
}
Mistake 3: Expecting Random Access on Filter View
filter creates a view that must search for the next matching element. Random access becomes O(n) instead of O(1).
Mistake 4: Forgetting Views are Lazy
Side effects in transform may execute at unexpected times (when iterating, not when defining the view).
Mistake 5: Chaining Too Many Views
Each view adds overhead. For performance-critical code, benchmark against traditional loops.
Mistake 6: Confusing ranges::sort with std::sort
ranges::sort(v) is equivalent to std::sort(v.begin(), v.end()). Both modify the container.
Practice Questions
- What is the difference between a view and a container?
- Why are views lazily evaluated?
- Write a pipeline that takes the first 5 even numbers from a vector, squares them, and prints the result.
- How would you create an infinite sequence of Fibonacci numbers using
views::iotaandviews::transform? - What is the pipe operator and how does it work?
Challenge
Create a custom view that generates prime numbers using std::views::iota and std::views::filter with the Sieve of Eratosthenes. Note that views are lazy, so the sieve can generate primes on demand.
FAQ
Mini Project
Build a data analysis pipeline using ranges:
#include <iostream>
#include <vector>
#include <ranges>
#include <numeric>
#include <cmath>
struct SensorReading {
double temperature;
double humidity;
int timestamp;
};
int main() {
std::vector<SensorReading> readings = {
{22.5, 45.0, 1000}, {23.1, 44.2, 1001}, {25.0, 50.0, 1002},
{21.0, 60.0, 1003}, {26.5, 35.0, 1004}, {22.0, 55.0, 1005},
{28.0, 30.0, 1006}, {24.0, 48.0, 1007}
};
auto valid = readings
| std::views::filter([](const SensorReading& r) {
return r.temperature > 0 && r.temperature < 50;
});
auto temps = valid | std::views::transform(&SensorReading::temperature);
double avgTemp = std::accumulate(temps.begin(), temps.end(), 0.0)
/ std::ranges::distance(temps);
std::cout << "Average temperature: " << avgTemp << "\n";
auto aboveAverage = temps
| std::views::filter([avgTemp](double t) { return t > avgTemp; });
std::cout << "Above-average temperatures:\n";
for (double t : aboveAverage) {
std::cout << " " << t << "\n";
}
// Top 3 humidity readings
auto topHumidity = readings
| std::views::transform(&SensorReading::humidity)
| std::views::take(3);
std::cout << "First 3 humidity readings: ";
for (double h : topHumidity) std::cout << h << " ";
std::cout << "\n";
}
What's Next
Ranges make data processing pipelines clean and efficient. The next lesson covers Iterator types in detail: input, output, forward, bidirectional, random access, contiguous iterators, and writing custom iterators.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro