The Ranges Library in C++20: Pipeline Operations

The C++20 Ranges library (<ranges>) extends the STL with a composable, lazy pipeline model for sequence operations. Range views are lightweight, lazy wrappers that transform, filter, and project sequences without copying data. They compose with the pipe operator |: data | views::filter(pred) | views::transform(fn) | views::take(n) applies filter, then transform, then take — evaluated lazily element-by-element only when iterated. Ranges also upgrade STL algorithms to accept single range arguments instead of iterator pairs.

Introduction

The classic STL algorithm model requires iterator pairs: std::sort(v.begin(), v.end()), std::find_if(v.begin(), v.end(), pred). This works, but composing multiple operations becomes verbose. To filter a vector, then transform each element, then take the first ten results, you would either write three separate passes (creating intermediate vectors), or craft a complex hand-rolled loop.

C++20’s Ranges library solves this with two innovations. First, range-based algorithms accept containers directly — std::ranges::sort(v) instead of std::ranges::sort(v.begin(), v.end()). They also support projections: std::ranges::sort(v, {}, &Person::name) sorts by the name field without a custom comparator.

Second, and more fundamentally, range views are lazy, composable transformations. A view describes what to do with elements but does no work until you iterate. Views compose with | into pipelines. v | views::filter(isEven) | views::transform(square) | views::take(5) reads from left to right like a sentence — “take v, keep the even elements, square each one, stop after 5” — with no intermediate allocations.

This article teaches the Ranges library from the ground up: range concepts, constrained algorithms, views and their lazy semantics, the pipe operator, the standard view library, and practical patterns for data processing pipelines.

Range Concepts: What Is a Range?

A range is anything with begin() and end() that return iterators. std::vector, std::list, std::string, C arrays, and custom types all qualify. C++20 formalizes this as a Concept:

#include <iostream>
#include <ranges>
#include <vector>
#include <list>
#include <array>
#include <string>
using namespace std;

// Demonstrating what qualifies as a range
template<ranges::range R>
void printRange(const R& r) {
    cout << "[ ";
    for (const auto& elem : r) cout << elem << " ";
    cout << "]\n";
}

int main() {
    // All of these are ranges:
    vector<int>   v = {1, 2, 3, 4, 5};
    array<int, 4> a = {10, 20, 30, 40};
    list<int>     l = {100, 200, 300};
    string        s = "hello";
    int           arr[] = {7, 8, 9};

    printRange(v);
    printRange(a);
    printRange(l);
    printRange(s);
    printRange(arr);

    // Range categories (concepts):
    cout << "\n--- Range category checks ---\n";

    // random_access_range: can jump to any element in O(1)
    cout << "vector is random_access_range: "
         << ranges::random_access_range<vector<int>> << "\n";

    // bidirectional_range: can iterate forward and backward
    cout << "list is bidirectional_range: "
         << ranges::bidirectional_range<list<int>> << "\n";
    cout << "list is random_access_range: "
         << ranges::random_access_range<list<int>> << "\n";

    // contiguous_range: elements are contiguous in memory (like array)
    cout << "vector is contiguous_range: "
         << ranges::contiguous_range<vector<int>> << "\n";
    cout << "list is contiguous_range:   "
         << ranges::contiguous_range<list<int>>   << "\n";

    // sized_range: know the size in O(1)
    cout << "vector is sized_range: "
         << ranges::sized_range<vector<int>> << "\n";

    // Range size and empty
    cout << "\nSize of v: " << ranges::size(v) << "\n";
    cout << "Empty v:   " << ranges::empty(v)  << "\n";

    vector<int> empty_v;
    cout << "Empty empty_v: " << ranges::empty(empty_v) << "\n";

    return 0;
}

Output:

[ 1 2 3 4 5 ]
[ 10 20 30 40 ]
[ 100 200 300 ]
[ h e l l o ]
[ 7 8 9 ]

--- Range category checks ---
vector is random_access_range: 1
list is bidirectional_range: 1
list is random_access_range: 0
vector is contiguous_range: 1
list is contiguous_range:   0
vector is sized_range: 1

Size of v: 5
Empty v:   0
Empty empty_v: 1

Step-by-step explanation:

  1. ranges::range<R> is a Concept that any type with begin()/end() satisfies. The printRange template is constrained to only accept ranges — a cleaner constraint than unconstrained typename T.
  2. Range categories form a hierarchy: contiguous_range ⊂ random_access_range ⊂ bidirectional_range ⊂ forward_range ⊂ input_range. A vector satisfies all; a list satisfies only up to bidirectional_range because you cannot jump to the Nth element in O(1).
  3. ranges::size(r) and ranges::empty(r) work uniformly on any sized or forward range. Unlike .size() and .empty() (member functions), these are free functions that work on C arrays too.
  4. The category determines which operations are available: binary search requires random_access_range; reverse requires bidirectional_range; find requires only input_range.

Constrained Algorithms: ranges:: vs std::

C++20 adds ranges:: versions of every standard algorithm that accept ranges instead of iterator pairs and support projections:

#include <iostream>
#include <ranges>
#include <algorithm>
#include <vector>
#include <string>
using namespace std;

struct Person {
    string name;
    int    age;
    double salary;
};

void printPeople(const vector<Person>& people, const string& label) {
    cout << label << ":\n";
    for (const auto& [name, age, salary] : people) {
        cout << "  " << name << " (age " << age << ", $" << (int)salary << ")\n";
    }
}

int main() {
    vector<Person> people = {
        {"Charlie", 35, 85000},
        {"Alice",   28, 95000},
        {"Bob",     42, 72000},
        {"Diana",   31, 110000},
        {"Eve",     28, 88000}
    };

    // --- Sorting with projection ---
    cout << "--- Sort by name ---\n";
    vector<Person> byName = people;
    ranges::sort(byName, {}, &Person::name);  // Project to name field
    printPeople(byName, "Sorted by name");

    cout << "\n--- Sort by salary descending ---\n";
    vector<Person> bySalary = people;
    ranges::sort(bySalary, greater<double>{}, &Person::salary);
    printPeople(bySalary, "Sorted by salary (desc)");

    cout << "\n--- Sort by age then name ---\n";
    vector<Person> byAgeName = people;
    ranges::sort(byAgeName, [](const Person& a, const Person& b) {
        if (a.age != b.age) return a.age < b.age;
        return a.name < b.name;
    });
    printPeople(byAgeName, "Sorted by age then name");

    // --- find_if with projection ---
    cout << "\n--- Find by projection ---\n";
    auto it = ranges::find(people, "Bob", &Person::name);
    if (it != people.end()) {
        cout << "Found: " << it->name << " age=" << it->age << "\n";
    }

    // --- count_if ---
    int youngCount = ranges::count_if(people, [](const Person& p) {
        return p.age < 35;
    });
    cout << "People under 35: " << youngCount << "\n";

    // --- any_of, all_of, none_of ---
    bool allAdults = ranges::all_of(people, [](const Person& p) {
        return p.age >= 18;
    });
    bool anyHighEarner = ranges::any_of(people, [](const Person& p) {
        return p.salary > 100000;
    });
    cout << "All adults:       " << allAdults    << "\n";
    cout << "Any high earner:  " << anyHighEarner << "\n";

    // --- min_element / max_element with projection ---
    auto youngest = ranges::min_element(people, {}, &Person::age);
    auto richest  = ranges::max_element(people, {}, &Person::salary);
    cout << "Youngest: " << youngest->name << " (age " << youngest->age << ")\n";
    cout << "Richest:  " << richest->name  << " ($" << (int)richest->salary << ")\n";

    // --- copy_if ---
    vector<Person> seniors;
    ranges::copy_if(people, back_inserter(seniors), [](const Person& p) {
        return p.age >= 35;
    });
    cout << "\nSeniors (35+):\n";
    for (const auto& [name, age, _] : seniors) {
        cout << "  " << name << " (age " << age << ")\n";
    }

    // --- transform with projection ---
    vector<string> names;
    ranges::transform(people, back_inserter(names), &Person::name);
    cout << "\nAll names: ";
    for (const auto& n : names) cout << n << " ";
    cout << "\n";

    // --- Comparison: old vs new style ---
    vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
    
    // Old style:
    sort(v.begin(), v.end());
    
    // New style (single argument, more concise):
    ranges::sort(v);
    
    cout << "\nSorted: ";
    for (int x : v) cout << x << " ";
    cout << "\n";

    return 0;
}

Output:

--- Sort by name ---
Sorted by name:
  Alice (age 28, $95000)
  Bob (age 42, $72000)
  Charlie (age 35, $85000)
  Diana (age 31, $110000)
  Eve (age 28, $88000)

--- Sort by salary descending ---
Sorted by salary (desc):
  Diana (age 31, $110000)
  Alice (age 28, $95000)
  Eve (age 28, $88000)
  Charlie (age 35, $85000)
  Bob (age 42, $72000)

--- Sort by age then name ---
  Alice (age 28, $95000)
  Eve (age 28, $88000)
  Diana (age 31, $110000)
  Charlie (age 35, $85000)
  Bob (age 42, $72000)

--- Find by projection ---
Found: Bob age=42

People under 35: 3
All adults:       1
Any high earner:  1
Youngest: Alice (age 28)
Richest:  Diana ($110000)

Seniors (35+):
  Charlie (age 35)
  Bob (age 42)

All names: Charlie Alice Bob Diana Eve 

Sorted: 1 1 2 3 4 5 6 9

Step-by-step explanation:

  1. ranges::sort(byName, {}, &Person::name) takes three arguments: the range, a comparator (default {} = less<>), and a projection (a callable or member pointer applied to each element before comparison). The projection &Person::name extracts the name from each Person for comparison.
  2. Projections eliminate the need for custom comparators in many cases. ranges::min_element(people, {}, &Person::age) finds the person with the minimum age — no lambda needed, just a member pointer.
  3. ranges::sort(v) (single-argument form) is identical to sort(v.begin(), v.end()). The range overloads are not separate functions — they are the same function with different overloads, using the unified range protocol.
  4. ranges::find(people, "Bob", &Person::name) projects each Person to its name, then finds the element where the projected value equals "Bob". Without projection, you’d write find_if(people.begin(), people.end(), [](const Person& p) { return p.name == "Bob"; }).
  5. The {} in ranges::sort(byName, {}, &Person::name) is a default-constructed comparator — less<>{}. You can pass greater<>{} for descending sort or any callable.

Views: Lazy, Composable Transformations

Views are the core innovation of the Ranges library. A view is a range that applies a transformation lazily — it does no work until elements are accessed during iteration.

#include <iostream>
#include <ranges>
#include <vector>
#include <string>
#include <numeric>
using namespace std;
namespace views = std::views;  // Alias for brevity

int main() {
    vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // --- views::filter ---
    cout << "=== filter ===\n";
    auto evens = data | views::filter([](int n) { return n % 2 == 0; });
    for (int n : evens) cout << n << " ";
    cout << "\n";

    // --- views::transform ---
    cout << "=== transform ===\n";
    auto squares = data | views::transform([](int n) { return n * n; });
    for (int n : squares) cout << n << " ";
    cout << "\n";

    // --- Composed pipeline ---
    cout << "=== filter | transform ===\n";
    auto evenSquares = data
        | views::filter([](int n) { return n % 2 == 0; })
        | views::transform([](int n) { return n * n; });
    for (int n : evenSquares) cout << n << " ";
    cout << "\n";  // 4 16 36 64 100

    // --- views::take and views::drop ---
    cout << "\n=== take and drop ===\n";
    auto first3 = data | views::take(3);
    cout << "First 3: ";
    for (int n : first3) cout << n << " ";
    cout << "\n";

    auto skip3 = data | views::drop(3);
    cout << "After 3: ";
    for (int n : skip3) cout << n << " ";
    cout << "\n";

    // --- views::take_while and views::drop_while ---
    auto lessThan5 = data | views::take_while([](int n) { return n < 5; });
    cout << "While < 5: ";
    for (int n : lessThan5) cout << n << " ";
    cout << "\n";

    // --- views::reverse ---
    cout << "\n=== reverse ===\n";
    auto rev = data | views::reverse;
    for (int n : rev) cout << n << " ";
    cout << "\n";

    // --- views::keys and views::values (for maps) ---
    cout << "\n=== keys and values ===\n";
    map<string, int> scores = {{"Alice", 95}, {"Bob", 72}, {"Carol", 88}};
    cout << "Keys: ";
    for (const auto& k : scores | views::keys) cout << k << " ";
    cout << "\n";
    cout << "Values: ";
    for (int v : scores | views::values) cout << v << " ";
    cout << "\n";

    // --- views::enumerate (C++23, but show the iota workaround) ---
    cout << "\n=== iota (generate sequence) ===\n";
    for (int n : views::iota(1, 11)) cout << n << " ";
    cout << "\n";

    // Generate squares of 1..10 without a source vector
    auto squaresOf10 = views::iota(1, 11)
        | views::transform([](int n) { return n * n; });
    cout << "Squares 1..10: ";
    for (int n : squaresOf10) cout << n << " ";
    cout << "\n";

    // --- views::join (flatten) ---
    cout << "\n=== join ===\n";
    vector<vector<int>> nested = {{1, 2, 3}, {4, 5}, {6, 7, 8, 9}};
    auto flat = nested | views::join;
    cout << "Flattened: ";
    for (int n : flat) cout << n << " ";
    cout << "\n";

    // --- views::split ---
    cout << "\n=== split ===\n";
    string csv = "alpha,beta,gamma,delta";
    for (auto word : csv | views::split(',')) {
        string_view sv(word.begin(), word.end());
        cout << sv << " ";
    }
    cout << "\n";

    // --- views::zip (C++23) workaround with iota ---
    cout << "\n=== zip-like with iota + transform ===\n";
    vector<string> names = {"Alice", "Bob", "Carol"};
    vector<int> ranks = {1, 2, 3};
    for (int i : views::iota(0, (int)names.size())) {
        cout << ranks[i] << ". " << names[i] << "\n";
    }

    return 0;
}

Output:

=== filter ===
2 4 6 8 10 
=== transform ===
1 4 9 16 25 36 49 64 81 100 
=== filter | transform ===
4 16 36 64 100 

=== take and drop ===
First 3: 1 2 3 
After 3: 4 5 6 7 8 9 10 
While < 5: 1 2 3 4 

=== reverse ===
10 9 8 7 6 5 4 3 2 1 

=== keys and values ===
Keys: Alice Bob Carol 
Values: 72 88 95 

=== iota (generate sequence) ===
1 2 3 4 5 6 7 8 9 10 
Squares 1..10: 1 4 9 16 25 36 49 64 81 100 

=== join ===
Flattened: 1 2 3 4 5 6 7 8 9 

=== split ===
alpha beta gamma delta 

=== zip-like with iota + transform ===
1. Alice
2. Bob
3. Carol

Step-by-step explanation:

  1. data | views::filter(pred) does not iterate data or allocate anything. It returns a view object that, when iterated, skips elements where pred returns false. Every element is evaluated lazily — one at a time, as the range-for loop requests them.
  2. The pipe | operator composes views. data | views::filter(pred) | views::transform(fn) creates a pipeline view. When iterated: fetch next element from data, apply filter, if it passes apply transform, yield the result. No intermediate container is created.
  3. views::iota(1, 11) generates the sequence {1, 2, 3, ..., 10} on demand — no storage required. Composing it with transform creates an infinite-or-finite generator pipeline with zero allocations.
  4. views::join flattens a range-of-ranges into a single range. nested | views::join iterates all inner vectors sequentially without copying.
  5. views::split(',') splits a string (or any range) by a delimiter, producing a view of sub-ranges. The sub-ranges are string views into the original — no allocation. Converting to string_view gives a lightweight view for printing.

Lazy Evaluation: Why It Matters

The key property of views is laziness — they do no work until elements are pulled from them:

#include <iostream>
#include <ranges>
#include <vector>
using namespace std;
namespace views = std::views;

int main() {
    // Demonstrate laziness: only processes elements actually consumed

    int filterCount    = 0;
    int transformCount = 0;

    vector<int> data(1'000'000);  // 1 million elements
    iota(data.begin(), data.end(), 1);  // Fill with 1..1000000

    // Lazy pipeline: filter evens, square them, take only first 5
    auto pipeline = data
        | views::filter([&](int n) {
            ++filterCount;
            return n % 2 == 0;
        })
        | views::transform([&](int n) {
            ++transformCount;
            return n * n;
        })
        | views::take(5);

    cout << "Pipeline created — no work done yet\n";
    cout << "filterCount: " << filterCount << "\n";    // 0
    cout << "transformCount: " << transformCount << "\n"; // 0

    cout << "\nIterating pipeline:\n";
    for (int n : pipeline) {
        cout << n << " ";
    }
    cout << "\n";

    cout << "\nAfter iteration of first 5 results:\n";
    cout << "filterCount:   " << filterCount    << "\n";  // ~10 (enough to find 5 evens)
    cout << "transformCount: " << transformCount << "\n"; // 5

    // Compare: eager approach
    int eagerFilter = 0, eagerTransform = 0;

    vector<int> step1;  // Eager filter
    for (int n : data) {
        ++eagerFilter;
        if (n % 2 == 0) step1.push_back(n);
    }

    vector<int> step2;  // Eager transform
    for (int n : step1) {
        ++eagerTransform;
        step2.push_back(n * n);
    }

    vector<int> step3(step2.begin(), step2.begin() + 5);  // Eager take

    cout << "\nEager approach to get same 5 results:\n";
    cout << "eagerFilter:    " << eagerFilter    << "\n";  // 1000000
    cout << "eagerTransform: " << eagerTransform << "\n";  // 500000
    cout << "Result: ";
    for (int n : step3) cout << n << " ";
    cout << "\n";

    return 0;
}

Output:

Pipeline created — no work done yet
filterCount: 0
transformCount: 0

Iterating pipeline:
4 16 36 64 100 

After iteration of first 5 results:
filterCount:   10
transformCount: 5

Eager approach to get same 5 results:
eagerFilter:    1000000
eagerTransform: 500000
Result: 4 16 36 64 100

Step-by-step explanation:

  1. Creating the pipeline does zero work — filterCount and transformCount are both 0 after setup. The view objects are just descriptions of what to do, not executions.
  2. When iterated, the pipeline processes only as many elements as needed: to find 5 even numbers, we only need to look at the first 10 elements (1, 2, 3, 4, 5, 6, 7, 8, 9, 10 — five odds filtered out, five evens found). The remaining 999,990 elements are never touched.
  3. The eager approach processes the entire 1,000,000 elements through filter and then 500,000 through transform — even though we only needed 5 results. This is 200,000x more work than the lazy pipeline.
  4. views::take(5) is the “short-circuit” — it signals the pipeline to stop after 5 results are produced. Without it, the lazy pipeline would process all elements too (just without intermediate allocations).
  5. Laziness also composes with infinite ranges: views::iota(1) | views::filter(isPrime) | views::take(10) generates the first 10 prime numbers from an infinite sequence — impossible with eager evaluation.

A Complete Data Processing Pipeline

#include <iostream>
#include <ranges>
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
#include <map>
using namespace std;
namespace views = std::views;

struct Employee {
    string  name;
    string  department;
    int     yearsExp;
    double  salary;
    bool    remote;
};

int main() {
    vector<Employee> employees = {
        {"Alice",   "Engineering", 8,  125000, true},
        {"Bob",     "Marketing",   3,  72000,  false},
        {"Carol",   "Engineering", 5,  98000,  true},
        {"Dave",    "HR",          2,  65000,  false},
        {"Eve",     "Engineering", 12, 148000, true},
        {"Frank",   "Marketing",   7,  88000,  false},
        {"Grace",   "Engineering", 1,  75000,  false},
        {"Henry",   "HR",          9,  95000,  true},
        {"Iris",    "Engineering", 4,  92000,  true},
        {"Jack",    "Marketing",   6,  82000,  true}
    };

    // --- Query 1: Senior remote engineers, sorted by salary ---
    cout << "=== Senior remote engineers (5+ years) ===\n";
    auto seniorRemote = employees
        | views::filter([](const Employee& e) {
            return e.department == "Engineering"
                && e.yearsExp >= 5
                && e.remote;
          })
        | views::transform([](const Employee& e) -> string {
            return e.name + " ($" + to_string((int)e.salary) + ")";
          });

    for (const auto& desc : seniorRemote) {
        cout << "  " << desc << "\n";
    }

    // --- Query 2: Top 3 earners across all departments ---
    cout << "\n=== Top 3 earners ===\n";
    vector<Employee> sorted = employees;
    ranges::sort(sorted, greater<double>{}, &Employee::salary);
    auto top3 = sorted | views::take(3);
    for (const auto& [name, dept, yrs, sal, rem] : top3) {
        cout << "  " << name << " (" << dept << "): $" << (int)sal << "\n";
    }

    // --- Query 3: Department salary averages ---
    cout << "\n=== Average salary by department ===\n";
    map<string, vector<double>> bySalary;
    for (const auto& e : employees) {
        bySalary[e.department].push_back(e.salary);
    }
    for (const auto& [dept, salaries] : bySalary) {
        double avg = accumulate(salaries.begin(), salaries.end(), 0.0)
                   / salaries.size();
        cout << "  " << dept << ": $" << (int)avg << "\n";
    }

    // --- Query 4: Names of marketing employees, alphabetically ---
    cout << "\n=== Marketing team (alphabetical) ===\n";
    auto marketingNames = employees
        | views::filter([](const Employee& e) {
            return e.department == "Marketing";
          })
        | views::transform(&Employee::name);

    vector<string> mNames(marketingNames.begin(), marketingNames.end());
    ranges::sort(mNames);
    for (const auto& n : mNames) cout << "  " << n << "\n";

    // --- Query 5: Count remote workers ---
    long remoteCount = ranges::count_if(employees, &Employee::remote);
    cout << "\nRemote workers: " << remoteCount << " / " << employees.size() << "\n";

    // --- Query 6: Generate employee IDs (iota + zip-like) ---
    cout << "\n=== Employee roster with IDs ===\n";
    for (auto [idx, emp] : views::iota(1001)
                         | views::take(employees.size())
                         | views::transform([&](int id) {
                             static int i = 0;
                             return pair{id, ref(employees[i++])};
                           })) {
        cout << "  ID-" << idx << ": " << emp.get().name << "\n";
    }

    // --- Query 7: Salary bands using transform and take_while ---
    cout << "\n=== Salary bands ===\n";
    auto highEarners = employees
        | views::filter([](const Employee& e) { return e.salary >= 100000; })
        | views::transform(&Employee::name);

    auto midEarners = employees
        | views::filter([](const Employee& e) {
            return e.salary >= 75000 && e.salary < 100000;
          })
        | views::transform(&Employee::name);

    cout << "High earners ($100k+): ";
    for (const auto& n : highEarners) cout << n << " ";

    cout << "\nMid earners ($75k-$100k): ";
    for (const auto& n : midEarners) cout << n << " ";
    cout << "\n";

    return 0;
}

Output:

=== Senior remote engineers (5+ years) ===
  Carol ($98000)
  Eve ($148000)
  Iris ($92000)

=== Top 3 earners ===
  Eve (Engineering): $148000
  Alice (Engineering): $125000
  Carol (Engineering): $98000

=== Average salary by department ===
  Engineering: $107600
  HR: $80000
  Marketing: $80666

=== Marketing team (alphabetical) ===
  Bob
  Frank
  Jack

Remote workers: 6 / 10

=== Employee roster with IDs ===
  ID-1001: Alice
  ID-1002: Bob
  ...

=== Salary bands ===
High earners ($100k+): Alice Eve 
Mid earners ($75k-$100k): Carol Henry Iris

Standard Views Reference

#include <iostream>
#include <ranges>
#include <vector>
#include <string>
using namespace std;
namespace views = std::views;

int main() {
    vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // --- views::counted ---
    cout << "counted(v.begin(), 4): ";
    for (int n : views::counted(v.begin(), 4)) cout << n << " ";
    cout << "\n";

    // --- views::all ---
    // Converts a range to a view (useful for explicit adaptation)
    auto allV = views::all(v);
    cout << "all size: " << ranges::size(allV) << "\n";

    // --- views::common ---
    // Makes begin/end the same type (needed for legacy algorithms)
    auto filtered = v | views::filter([](int n) { return n > 5; });
    auto common = filtered | views::common;
    // Now can use with std:: algorithms that require same iterator type:
    cout << "sum of >5: "
         << accumulate(common.begin(), common.end(), 0) << "\n";

    // --- views::elements (for tuple-like ranges) ---
    vector<tuple<int, string, double>> records = {
        {1, "Alice", 95.0},
        {2, "Bob",   72.0},
        {3, "Carol", 88.0}
    };

    cout << "IDs:    ";
    for (auto id : records | views::elements<0>) cout << id << " ";
    cout << "\n";
    cout << "Names:  ";
    for (const auto& name : records | views::elements<1>) cout << name << " ";
    cout << "\n";
    cout << "Scores: ";
    for (auto score : records | views::elements<2>) cout << score << " ";
    cout << "\n";

    // --- Materializing a view into a container (C++23: ranges::to) ---
    // C++20 way: construct vector from view
    auto evenSquares = v
        | views::filter([](int n) { return n % 2 == 0; })
        | views::transform([](int n) { return n * n; });

    vector<int> result(evenSquares.begin(), evenSquares.end());
    cout << "Even squares: ";
    for (int n : result) cout << n << " ";
    cout << "\n";

    // --- Passing a view to a ranges:: algorithm ---
    auto pipeline = v | views::transform([](int n) { return n * 3; });
    auto maxVal = ranges::max_element(pipeline);
    cout << "Max of v*3: " << *maxVal << "\n";

    return 0;
}

Output:

counted(v.begin(), 4): 1 2 3 4 
all size: 10
sum of >5: 40
IDs:    1 2 3 
Names:  Alice Bob Carol 
Scores: 95 72 88 
Even squares: 4 16 36 64 100 
Max of v*3: 30

Step-by-step explanation:

  1. views::counted(iterator, n) creates a view of exactly n elements starting at iterator — useful when you have an iterator and count but not an end iterator.
  2. views::common adapts a view whose begin and end return different types (common with filter views) to a view where they return the same type. This is required for compatibility with pre-C++20 STL algorithms that expect iterator == sentinel.
  3. views::elements<N> projects a range of tuple-like elements to their Nth element — similar to views::keys (N=0) and views::values (N=1) but for any tuple position.
  4. Materializing a view into a container uses the range constructor: vector<T> result(view.begin(), view.end()). C++23 adds ranges::to<vector>() for cleaner syntax.
  5. Views compose with ranges:: algorithms — ranges::max_element(pipeline) finds the maximum element of a transformed view without materializing it.

Views Quick Reference

View Description Example
views::filter(pred) Keep elements where pred is true v | views::filter(isEven)
views::transform(fn) Apply fn to each element v | views::transform(square)
views::take(n) First n elements v | views::take(5)
views::drop(n) Skip first n elements v | views::drop(3)
views::take_while(pred) Elements while pred is true v | views::take_while(lt10)
views::drop_while(pred) Skip elements while pred is true v | views::drop_while(lt10)
views::reverse Elements in reverse order v | views::reverse
views::keys Keys of a pair/map range m | views::keys
views::values Values of a pair/map range m | views::values
views::elements<N> Nth element of tuple-like range v | views::elements<2>
views::join Flatten range of ranges nested | views::join
views::split(delim) Split range by delimiter s | views::split(',')
views::iota(start, end) Integer sequence [start, end) views::iota(1, 11)
views::iota(start) Infinite integer sequence views::iota(0)
views::counted(it, n) n elements from iterator views::counted(it, 5)
views::all(r) Convert range to view views::all(container)
views::common Homogenize iterator types v | views::common

Common Mistakes

Mistake 1: Storing a view that references a destroyed container.

auto getView() {
    vector<int> v = {1, 2, 3};
    return v | views::filter(isEven);  // DANGLING: v is destroyed at return
}
auto view = getView();
for (int n : view) cout << n;  // UB: iterating dangling view
// Fix: return the materialized vector, not a view of a local

Mistake 2: Modifying a container while iterating a view over it.

vector<int> v = {1, 2, 3, 4, 5};
auto view = v | views::filter(isEven);
v.push_back(6);  // May reallocate v — view now dangles
for (int n : view) cout << n;  // UB
// Fix: don't modify the source while iterating a view

Mistake 3: Expecting views to be const-safe like containers.

void process(const vector<int>& v) {
    // views::filter on a const range — fine, elements are const
    for (int n : v | views::filter(isEven)) cout << n << " ";
}
// But: some views are not const-iterable (notably filter_view)
// Use auto& (non-const) when assigning views if you'll iterate them
auto view = v | views::filter(pred);  // OK
const auto view2 = v | views::filter(pred);
for (int n : view2) {}  // May fail to compile for filter_view

Mistake 4: Using a sentinel-based view with pre-C++20 algorithms.

auto filtered = v | views::filter(pred);
// OLD algorithms expect same type for begin/end:
accumulate(filtered.begin(), filtered.end(), 0);  // May not compile
// Fix: use views::common or ranges:: algorithms
accumulate((filtered | views::common).begin(),
           (filtered | views::common).end(), 0);
// Better: use ranges::fold_left (C++23) or equivalent

Mistake 5: Assuming views copy data.

auto view = bigVector | views::transform(expensiveFn);
auto view2 = view;  // O(1): views are lightweight wrappers, not copies
// But iterating view2 twice calls expensiveFn twice each time
// If you need the results stored, materialize: vector<T> v(view.begin(), view.end())

Conclusion

The C++20 Ranges library transforms how you write sequence operations in C++. Instead of explicit loops, iterator pairs, and intermediate containers, you compose declarative pipelines that read naturally and execute efficiently.

The two pillars of the library work together. ranges:: algorithms accept whole ranges and support projections — ranges::sort(people, {}, &Person::name) replaces a custom comparator with a member pointer. Views compose lazily with | — data | views::filter(pred) | views::transform(fn) | views::take(n) evaluates on demand, processing only the elements needed, with no intermediate allocations.

Laziness is ranges’ most important property. A million-element pipeline that only consumes 5 results processes only those 5 — not the full million. This is what makes ranges genuinely composable: you can chain as many views as needed without paying for the work not done.

Practical benefits are immediate: map iteration with views::keys and views::values, string splitting with views::split, sequence generation with views::iota, flattening with views::join, and window operations with views::take_while. Combined with structured bindings and constrained algorithms, the Ranges library makes C++ data processing code concise, correct, and expressive — closer to what you would write in a functional language, but with C++’s performance characteristics.

C++23 extends the library further with views::zip, views::chunk, views::slide, views::adjacent, ranges::to<Container>, and monadic operations on views. The trajectory is clear: ranges are the future of C++ sequence processing.

Hot this week

SIMD Programming in C++: A Comprehensive Guide to Vectorization

SIMD (Single Instruction, Multiple Data) programming in C++ is...

Writing Cache-Friendly C++ Code

Learn to write cache-friendly C++ code — understand CPU caches, cache lines, spatial and temporal locality, data-oriented design, struct layout, false sharing, and how to measure cache performance.

Understanding Undefined Behavior in C++

Master C++ undefined behavior — learn what it is, the most dangerous forms (signed overflow, null dereference, data races, UB in templates), how compilers exploit it, and how to detect and eliminate it.

CMake Mastery: Modern C++ Build Systems

Master CMake for modern C++ projects — learn targets, properties, find_package, FetchContent, generator expressions, testing with CTest, and professional project structure.

Building Cross-Platform C++ Applications

Learn to build cross-platform C++ applications — handle OS differences, use CMake, manage compiler quirks, abstract platform APIs, write portable code, and test on multiple targets.

Topics

SIMD Programming in C++: A Comprehensive Guide to Vectorization

SIMD (Single Instruction, Multiple Data) programming in C++ is...

Writing Cache-Friendly C++ Code

Learn to write cache-friendly C++ code — understand CPU caches, cache lines, spatial and temporal locality, data-oriented design, struct layout, false sharing, and how to measure cache performance.

Understanding Undefined Behavior in C++

Master C++ undefined behavior — learn what it is, the most dangerous forms (signed overflow, null dereference, data races, UB in templates), how compilers exploit it, and how to detect and eliminate it.

CMake Mastery: Modern C++ Build Systems

Master CMake for modern C++ projects — learn targets, properties, find_package, FetchContent, generator expressions, testing with CTest, and professional project structure.

Building Cross-Platform C++ Applications

Learn to build cross-platform C++ applications — handle OS differences, use CMake, manage compiler quirks, abstract platform APIs, write portable code, and test on multiple targets.

Coroutines in C++20: Asynchronous Programming

Master C++20 coroutines — learn co_await, co_yield, co_return, promise types, awaitables, generators, and how to build async tasks and lazy sequences without callback hell.

std::optional, std::variant, and std::any in Modern C++

Master C++17's three vocabulary types — std::optional for nullable values, std::variant for type-safe unions, and std::any for type-erased storage, with practical examples and best practices.

C++17 Structured Bindings: Unpacking Data

Master C++17 structured bindings — learn how to unpack tuples, pairs, arrays, and structs into named variables, use them in range-for loops, and extend them to custom types.

Related Articles

Popular Categories