C++ Ranges Library
Modern C++20/23 approach to working with sequences
🌊 What are C++ Ranges?
C++ Ranges (C++20/23) provide a modern, composable way to work with sequences of data using functional programming concepts, making code more readable and expressive than traditional iterators.
#include <ranges>
#include <vector>
std::vector<int> nums = {1, 2, 3, 4, 5};
auto even = nums | std::views::filter([](int n) { return n % 2 == 0; });
Core Range Concepts
Range
Any object you can iterate over
std::vector<int> vec = {1, 2, 3};
View
Lazy, non-owning range adaptor
auto view = vec | std::views::take(2);
Pipeline
Chain operations with | operator
vec | filter(...) | transform(...);
Lazy Evaluation
Operations execute only when needed
// No work done until iteration
🔹 Basic Range Operations
Basic range operations in C++20 Ranges allow filtering, querying, and inspecting sequences without modifying them directly. For example, checking how many numbers in a range exceed a value (like 30) is a common filtering task. Similarly, verifying whether all elements satisfy a condition—such as being positive—is a useful query. These operations form the foundation of the Ranges library, providing a more expressive and composable alternative to traditional loops and algorithms. Ranges work seamlessly with standard containers and can dramatically improve code clarity and maintainability in data processing tasks.
#include <ranges>
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> numbers = {64, 34, 25, 12, 22, 11, 90};
// Sort using ranges
std::ranges::sort(numbers);
// Find using ranges
auto it = std::ranges::find(numbers, 25);
if (it != numbers.end()) {
std::cout << "Found: " << *it << std::endl;
}
// Count elements
auto count = std::ranges::count_if(numbers,
[](int n) { return n > 30; });
std::cout << "Numbers > 30: " << count << std::endl;
// Check if all elements satisfy condition
bool all_positive = std::ranges::all_of(numbers,
[](int n) { return n > 0; });
return 0;
}
Output:
Found: 25
Numbers > 30: 3
All positive: true
🔹 Range Views - Lazy Operations
Range views in C++20 are lazy, non-owning adaptors that transform or filter ranges without immediate
computation or copying. Operations like views::take(5) limit a range to its first five
elements, while views::filter can select only even numbers. This laziness means computations are
deferred until iteration, improving performance for large or infinite sequences. Views can be composed into
pipelines, enabling efficient, readable data transformations. This declarative style is a key advantage of modern
C++, reducing boilerplate and potential errors compared to manual loop-based implementations.
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Take first 5 elements
auto first_five = numbers | std::views::take(5);
// Drop first 3 elements
auto skip_three = numbers | std::views::drop(3);
// Filter even numbers
auto evens = numbers | std::views::filter([](int n) { return n % 2 == 0; });
// Transform (square each number)
auto squares = numbers | std::views::transform([](int n) { return n * n; });
// Print results
std::cout << "First five: ";
for (int n : first_five) {
std::cout << n << " ";
}
std::cout << std::endl;
std::cout << "Even numbers: ";
for (int n : evens) {
std::cout << n << " ";
}
std::cout << std::endl;
return 0;
}
Output:
First five: 1 2 3 4 5
Even numbers: 2 4 6 8 10
🔹 Composing Range Pipelines
Composing range pipelines involves chaining multiple view operations into a single, readable expression for
complex data processing. For instance, you can filter, transform, and then take elements in one fluent
sequence. A pipeline like numbers | views::filter(is_even) | views::transform(square) first selects
even numbers and then squares each result. This approach promotes clean, functional-style code and enhances
performance through lazy evaluation. Such pipelines are central to the Ranges library's power, allowing developers
to express sophisticated data manipulations concisely and efficiently.
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
// Complex pipeline: filter evens, square them, take first 3
auto result = data
| std::views::filter([](int n) { return n % 2 == 0; }) // Even numbers
| std::views::transform([](int n) { return n * n; }) // Square them
| std::views::take(3); // Take first 3
std::cout << "Pipeline result: ";
for (int n : result) {
std::cout << n << " ";
}
std::cout << std::endl;
// Another pipeline: skip first 5, filter odds, double them
auto another = data
| std::views::drop(5) // Skip first 5
| std::views::filter([](int n) { return n % 2 == 1; }) // Odd numbers
| std::views::transform([](int n) { return n * 2; }); // Double them
std::cout << "Another pipeline: ";
for (int n : another) {
std::cout << n << " ";
}
std::cout << std::endl;
return 0;
}
Output:
Pipeline result: 4 16 36
Another pipeline: 12 14 16 18 20 22 24
🔹 Advanced Range Views
Advanced range views support sophisticated manipulations like enumerating, reversing, and flattening nested
sequences. The views::enumerate view pairs each element with its index, useful for indexed
iteration. views::reverse iterates over a range in reverse order without modifying the underlying data.
views::join or flattening views combine nested ranges (e.g., a vector of vectors) into a single flat
sequence. These tools expand the Ranges library's applicability, enabling elegant solutions to common problems in
data analysis, algorithm implementation, and sequence processing.
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
int main() {
std::vector<std::string> words = {"hello", "world", "cpp", "ranges", "are", "awesome"};
// Enumerate - get index and value
auto enumerated = words | std::views::enumerate;
std::cout << "Enumerated: ";
for (auto [index, word] : enumerated) {
std::cout << index << ":" << word << " ";
}
std::cout << std::endl;
// Reverse view
auto reversed = words | std::views::reverse;
std::cout << "Reversed: ";
for (const auto& word : reversed) {
std::cout << word << " ";
}
std::cout << std::endl;
// Join strings with separator (C++23)
std::vector<std::vector<int>> nested = {{1, 2}, {3, 4}, {5, 6}};
auto flattened = nested | std::views::join;
std::cout << "Flattened: ";
for (int n : flattened) {
std::cout << n << " ";
}
std::cout << std::endl;
return 0;
}
Output:
Enumerated: 0:hello 1:world 2:cpp 3:ranges 4:are 5:awesome
Reversed: awesome are ranges cpp world hello
Flattened: 1 2 3 4 5 6
🔹 Converting Views to Containers
While views are lazy and non-owning, sometimes a materialized, concrete container is needed for storage or
repeated use. You can convert a view to a container using range constructors or algorithms like
std::vector<T>(view.begin(), view.end()) or the ranges::to utility in C++23.
Materialization triggers evaluation of the entire pipeline, storing the results. This is essential when you need to
own the data, pass it to APIs expecting containers, or avoid recomputation. Understanding when to materialize is key
to balancing performance and functionality in range-based code.
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> source = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Create a view pipeline
auto view = source
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
// Convert view to vector (C++23)
std::vector<int> result(view.begin(), view.end());
// Or using ranges::to (C++23)
// auto result = view | std::ranges::to<std::vector>();
std::cout << "Materialized result: ";
for (int n : result) {
std::cout << n << " ";
}
std::cout << std::endl;
// You can also collect into other containers
std::cout << "View elements: ";
for (int n : view) {
std::cout << n << " ";
}
std::cout << std::endl;
return 0;
}
Output:
Materialized result: 4 16 36 64 100
View elements: 4 16 36 64 100