C++ Algorithms & Ranges: Views, Pipelines, Projections & std::ranges (C++20/23)

C++ Algorithms & Ranges: Views, Pipelines, Projections & std::ranges (C++20/23)
Table of Contents
- Ranges Concepts: Range, View, and Viewable Range
- std::ranges Algorithms: Drop the Iterator Pairs
- Projections: Searching and Sorting by Member
- Views: Lazy Evaluation Pipelines
- View Composition with the Pipe Operator
- Essential Views Reference
- std::ranges::to: Materializing Views (C++23)
- Infinite and Generated Ranges
- Parallel Algorithms: std::execution Policies
- Custom Range Adaptors
- Frequently Asked Questions
- Key Takeaway
Ranges Concepts: Range, View, and Viewable Range
A View is a range that:
- Copies in O(1) — it's a non-owning reference, like
std::string_view - Computes lazily — no data is processed until iterated
- Composes with other views via
|
std::ranges Algorithms: Drop the Iterator Pairs
Every std:: algorithm has a std::ranges:: equivalent that accepts the whole container:
#include <algorithm>
#include <ranges>
#include <vector>
#include <string>
std::vector<int> nums = {5, 2, 8, 1, 9, 3};
std::vector<std::string> words = {"banana", "apple", "cherry"};
// === SORTING ===
std::sort(nums.begin(), nums.end()); // Old: iterator pair
std::ranges::sort(nums); // New: entire range
std::ranges::sort(nums, std::greater{}); // Descending
// === FINDING ===
auto it = std::find(nums.begin(), nums.end(), 8); // Old
auto it2 = std::ranges::find(nums, 8); // New — same iterator
bool has = std::ranges::contains(nums, 8); // C++23: boolean directly
// === COUNTING ===
auto count = std::ranges::count(nums, 3);
auto cond = std::ranges::count_if(nums, [](int n){ return n > 5; });
// === PARTITIONING & REMOVING ===
auto part = std::ranges::partition(nums, [](int n){ return n % 2 == 0; });
// Erase-remove idiom — C++20 replaces it:
std::erase_if(nums, [](int n){ return n < 3; }); // C++20: one call!
// === COPYING & TRANSFORMING ===
std::vector<int> doubled;
std::ranges::transform(nums, std::back_inserter(doubled), [](int n){ return n * 2; });
// === CHECKING PREDICATES ===
bool all_pos = std::ranges::all_of(nums, [](int n){ return n > 0; });
bool any_big = std::ranges::any_of(nums, [](int n){ return n > 7; });
bool none_neg = std::ranges::none_of(nums, [](int n){ return n < 0; });
// === MIN/MAX ===
auto [min_it, max_it] = std::ranges::minmax_element(nums);
int min_val = *min_it, max_val = *max_it;Projections: Searching and Sorting by Member
Projections are one of the best quality-of-life features in std::ranges. Instead of writing a custom comparator lambda, you pass a member pointer or callable as the projection:
struct Player {
std::string name;
int score;
int level;
};
std::vector<Player> players = {
{"Alice", 95, 10},
{"Bob", 82, 8},
{"Carol", 78, 12},
};
// Sort by score (descending) using projection:
std::ranges::sort(players, std::greater{}, &Player::score);
// Without projection (old way):
std::sort(players.begin(), players.end(),
[](const Player& a, const Player& b){ return a.score > b.score; });
// Find by name using projection:
auto it = std::ranges::find(players, "Alice", &Player::name);
if (it != players.end()) std::cout << it->score << '\n'; // 95
// min/max by level:
auto& low_level = *std::ranges::min_element(players, {}, &Player::level);
auto& high_level = *std::ranges::max_element(players, {}, &Player::level);
// Check if sorted by score:
bool sorted_by_score = std::ranges::is_sorted(players, {}, &Player::score);Views: Lazy Evaluation Pipelines
Views are lazy: they define how to process data but only do the work when you iterate:
#include <ranges>
#include <vector>
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Each view creates a "recipe" — no data copied, no computation yet:
auto pipeline = data
| std::views::filter([](int n){ return n % 2 == 0; }) // Evens only
| std::views::transform([](int n){ return n * n; }) // Square them
| std::views::take(3); // First 3
// Computation happens HERE — only when iterated:
for (int x : pipeline) { std::cout << x << ' '; }
// Output: 4 16 36 (squares of 2, 4, 6)
// ONLY 6 elements evaluated from data (stopped at take(3))
// Memory: ZERO intermediate vectors allocated!
// Equivalent loop the compiler generates:
// int count = 0;
// for (int n : data) {
// if (count == 3) break;
// if (n % 2 == 0) { print(n * n); count++; }
// }Essential Views Reference
#include <ranges>
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// === Filtering ===
auto evens = v | std::views::filter([](int n){ return n % 2 == 0; });
// === Transformation ===
auto squares = v | std::views::transform([](int n){ return n * n; });
// === Slicing ===
auto first3 = v | std::views::take(3); // First 3: {1,2,3}
auto last3 = v | std::views::take_while([](int n){ return n <= 3; });
auto skip3 = v | std::views::drop(3); // Skip first 3: {4,5,...}
auto slice = v | std::views::drop(2) | std::views::take(4); // {3,4,5,6}
// === Reversal ===
auto rev = v | std::views::reverse;
// === Keys/Values (map) ===
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 82}};
for (auto& key : scores | std::views::keys) { std::cout << key << ' '; }
for (auto& value : scores | std::views::values) { std::cout << value << ' '; }
// === Enumerate: index + value (C++23) ===
for (auto [i, val] : v | std::views::enumerate) {
std::cout << i << ": " << val << '\n';
}
// === Zip: parallel iteration (C++23) ===
std::vector<std::string> names = {"Alice", "Bob", "Carol"};
for (auto [name, score] : std::views::zip(names, v)) {
std::println("{}: {}", name, score);
}
// === Split and join ===
std::string csv = "a,b,c,d";
for (auto part : csv | std::views::split(',')) {
std::string s(part.begin(), part.end());
std::cout << s << ' '; // "a" "b" "c" "d"
}
// === Chunk (C++23) ===
auto chunks = v | std::views::chunk(3); // {{1,2,3}, {4,5,6}, {7,8,9}, {10}}std::ranges::to: Materializing Views (C++23)
Views are lazy — to get a concrete std::vector, use std::ranges::to (C++23):
#include <ranges>
#include <vector>
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// C++23: materialize view into vector
auto evens = data
| std::views::filter([](int n){ return n % 2 == 0; })
| std::ranges::to<std::vector>(); // Creates std::vector<int>{2,4,6,8,10}
// Into other containers:
auto even_set = data
| std::views::filter([](int n){ return n % 2 == 0; })
| std::ranges::to<std::set<int>>();
// Pre-C++23: manual collection
std::vector<int> collected;
for (int n : data | std::views::filter([](int n){ return n % 2 == 0; }))
collected.push_back(n);Parallel Algorithms: std::execution Policies
Standard algorithms support parallel execution via execution policies (requires TBB or compiler parallelism support):
#include <algorithm>
#include <execution>
#include <vector>
std::vector<int> big_data(10'000'000);
std::iota(big_data.begin(), big_data.end(), 0);
// Sequential (default, guaranteed serial):
std::sort(std::execution::seq, big_data.begin(), big_data.end());
// Parallel (may use multiple threads):
std::sort(std::execution::par, big_data.begin(), big_data.end());
// Parallel + SIMD vectorized:
std::sort(std::execution::par_unseq, big_data.begin(), big_data.end());
// Parallel transform:
std::transform(std::execution::par_unseq,
big_data.begin(), big_data.end(), big_data.begin(),
[](int n){ return n * n; }); // Vectorized, parallelized!
// reduce (parallel sum) — not accumulate! accumulate is always serial
auto total = std::reduce(std::execution::par_unseq,
big_data.begin(), big_data.end(), 0LL);
// WARNING: par/par_unseq algorithms must use thread-safe lambdas —
// no shared mutable state without synchronization!Frequently Asked Questions
Do range views incur overhead compared to manual loops?
No — the compiler fuses the entire view pipeline into a single optimized loop. The generated assembly for filter | transform | take is identical to a handwritten for loop with inline conditions. Views add zero runtime overhead vs manual loops; their only cost is slightly longer compile times.
Can I use ranges with my custom container?
Yes — any container with begin() and end() returning iterators is automatically a range. For the full std::ranges feature set (projections, etc.), your iterators should model the std::input_iterator concept (or stronger). std::views::iota and std::views::generate create ranges from arbitrary generators without needing a container.
What is the difference between std::ranges::sort and std::sort?
Both sort in-place. std::ranges::sort accepts the whole container (no begin/end needed), supports projections, requires std::random_access_range, and returns the sorted subrange. std::sort requires explicit iterator pairs and doesn't support projections. In new code, always prefer std::ranges::sort.
Key Takeaway
The std::ranges library is the biggest productivity and correctness improvement to C++ data processing since C++11 containers. Lazy views eliminate intermediate allocations. Projections eliminate comparator lambdas. Range algorithms eliminate begin()/end() boilerplate. Parallel execution policies add multi-core performance with a single argument change. Together, they enable Python-level expressiveness at C++ performance.
Read next: Safety First: std::span and Bounds Checking →
*Part of the C++ Mastery Course — 30 modules from modern C++ basics to expert systems engineer
