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

🧠 Test Your Knowledge

What operator is used to chain range operations together?