Skip to content

Ranges Library — std::ranges, Views, Pipe Operator, Range Adaptors

DodaTech Updated 2026-06-28 8 min read

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

  1. What is the difference between a view and a container?
  2. Why are views lazily evaluated?
  3. Write a pipeline that takes the first 5 even numbers from a vector, squares them, and prints the result.
  4. How would you create an infinite sequence of Fibonacci numbers using views::iota and views::transform?
  5. 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

Do ranges have performance overhead?

Views are lightweight (usually a pointer and a size), but chaining many views adds indirection. Compilers are good at optimizing simple view chains into efficient loops.

What compiler support is needed for ranges?

GCC 10+, Clang 13+, MSVC 16.10+ have good ranges support. Some features (views::split, views::join) require later versions.

Can I use ranges with `std::array` and C arrays?

Yes. Ranges work with any range, including arrays. std::ranges::sort(my_array) works.

What is the difference between `std::ranges::view` and `std::ranges::range`?

A range is anything you can iterate. A view is a lightweight range that is cheap to copy, move, and assign. All views are ranges, but not all ranges are views.

How do I convert a view back to a container?

Use std::ranges::copy(view, std::back_inserter(container)) or a container constructor that accepts a range (available in C++23 for most containers).

What are 'borrowed ranges'?

A borrowed range is one that does not own its elements. When you take an iterator from a borrowed range, the iterator remains valid even after the range object is destroyed.

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