C++ Ranges & Views: filter, transform, Pipelines & Lazy Evaluation (C++20) Complete Masterclass

โšก Modern C++ (C++17 / C++20 / C++23) ๐ŸŸข Lesson 21 ๐Ÿ“‚ Phase 21: Ranges & Views (C++20) ๐Ÿ“… 2026 Master Edition
๐Ÿ“Œ Covered in this in-depth guide: Ranges vs Iterator Pairs ยท Projections ยท std::ranges algorithms ยท views::filter & transform ยท views::take & drop ยท take_while & drop_while ยท views::iota (finite & infinite) ยท Pipeline Composition | ยท views::keys & values ยท Lazy Evaluation ยท Materializing Views ยท C++23 zip & enumerate

Welcome to Phase 21 (Chapter 21): C++ Ranges & Views (C++20) Masterclass! The Ranges library is the biggest quality-of-life improvement in C++ since C++11. Ranges eliminate iterator pairs from algorithm calls, views enable lazy composable pipelines with the | operator, and the whole system is zero-cost due to lazy evaluation and compile-time composition.

1What are Ranges? The Big Picture

A range is any type with begin() and end(). The std::ranges namespace provides range-based versions of all STL algorithms that accept a single range instead of a begin/end pair โ€” eliminating a whole class of bugs.

Old STL vs Ranges:

Old: std::sort(v.begin(), v.end()); โ€” requires begin/end pair, can be mismatched

New: std::ranges::sort(v); โ€” accepts the range directly, impossible to mismatch

Old: std::sort(v.begin(), v.end(), pred);

New: std::ranges::sort(v, pred); โ€” cleaner, composable with projections

C++ โ€” std::ranges algorithms (C++20)โ–ถ Run in Compiler
#include <iostream>
#include <vector>
#include <algorithm>
#include <ranges>
#include <string>

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

int main() {
    std::vector<int> nums{5, 2, 8, 1, 9, 3, 7, 4, 6};

    // Ranges algorithms โ€” no begin/end, range-based projections!
    std::ranges::sort(nums);
    std::cout << "sorted: ";
    for (int n : nums) std::cout << n << " ";
    std::cout << "
";

    std::ranges::reverse(nums);
    std::cout << "reversed: ";
    for (int n : nums) std::cout << n << " ";
    std::cout << "
";

    // find with predicate
    auto it = std::ranges::find(nums, 7);
    if (it != nums.end())
        std::cout << "7 at index: " << std::distance(nums.begin(), it) << "
";

    // count_if
    int evens = std::ranges::count_if(nums, [](int n){ return n%2==0; });
    std::cout << "evens: " << evens << "
";

    // โ”€โ”€โ”€ Projections โ€” apply transform before comparison โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    std::vector<Person> people{
        {"Charlie", 35, 90000}, {"Alice", 28, 75000},
        {"Bob", 42, 120000}, {"Diana", 31, 95000}
    };

    // Sort by age using projection (much cleaner than lambda comparator!)
    std::ranges::sort(people, {}, &Person::age);
    std::cout << "
Sorted by age:
";
    for (const auto& p : people)
        std::cout << "  " << p.name << " (" << p.age << ")
";

    // Sort by name length using projection
    std::ranges::sort(people, std::ranges::less{}, [](const Person& p){ return p.name.size(); });
    std::cout << "
Sorted by name length:
";
    for (const auto& p : people) std::cout << "  " << p.name << "
";

    // Find max salary using projection
    auto richest = std::ranges::max_element(people, {}, &Person::salary);
    std::cout << "
Highest salary: " << richest->name << " ($" << richest->salary << ")
";

    return 0;
}
2Views โ€” Lazy Composable Pipelines

Views are lazy, non-owning range adapters. They compute elements on-demand โ€” no intermediate containers, no copies. Multiple views are chained with the pipe | operator into composable pipelines.

C++ โ€” Views: filter, transform, take, drop, iota, reverseโ–ถ Run in Compiler
#include <iostream>
#include <ranges>
#include <vector>
#include <string>
#include <algorithm>

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

    // โ”€โ”€โ”€ Basic views โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    // filter โ€” keep only matching elements
    auto evens = numbers | std::views::filter([](int n){ return n%2==0; });
    std::cout << "evens: ";
    for (int n : evens) std::cout << n << " ";
    std::cout << "
";

    // transform โ€” map each element
    auto squares = numbers | std::views::transform([](int n){ return n*n; });
    std::cout << "squares: ";
    for (int n : squares) std::cout << n << " ";
    std::cout << "
";

    // take โ€” first N elements
    auto first5 = numbers | std::views::take(5);
    std::cout << "take(5): ";
    for (int n : first5) std::cout << n << " ";
    std::cout << "
";

    // drop โ€” skip first N elements
    auto after3 = numbers | std::views::drop(3);
    std::cout << "drop(3): ";
    for (int n : after3) std::cout << n << " ";
    std::cout << "
";

    // take_while / drop_while
    auto lessThan6 = numbers | std::views::take_while([](int n){ return n<6; });
    std::cout << "take_while(<6): ";
    for (int n : lessThan6) std::cout << n << " ";
    std::cout << "
";

    // reverse
    auto reversed = numbers | std::views::reverse;
    std::cout << "reversed: ";
    for (int n : reversed) std::cout << n << " ";
    std::cout << "
";

    // โ”€โ”€โ”€ Composed pipeline โ€” chaining multiple views โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    auto pipeline = numbers
        | std::views::filter([](int n){ return n%2==0; })      // keep evens
        | std::views::transform([](int n){ return n*n; })       // square them
        | std::views::take(4);                                   // take first 4
    std::cout << "even squares (first 4): ";
    for (int n : pipeline) std::cout << n << " ";              // 4 16 36 64
    std::cout << "
";

    // โ”€โ”€โ”€ iota โ€” generate integer sequence โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    // Finite: [0, 10)
    std::cout << "iota(0,10): ";
    for (int n : std::views::iota(0, 10)) std::cout << n << " ";
    std::cout << "
";

    // Infinite iota + take (lazy โ€” only computes what's needed!)
    auto firstNFibs = std::views::iota(1)
        | std::views::take(8)
        | std::views::transform([](int n){ return n*n; });
    std::cout << "1^2 to 8^2: ";
    for (int n : firstNFibs) std::cout << n << " ";
    std::cout << "
";

    // โ”€โ”€โ”€ keys and values views on map โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    #include <map>
    std::map<std::string, int> scores{{"Alice",95},{"Bob",87},{"Charlie",92}};

    std::cout << "keys: ";
    for (const auto& k : scores | std::views::keys) std::cout << k << " ";
    std::cout << "
";

    std::cout << "values: ";
    for (auto v : scores | std::views::values) std::cout << v << " ";
    std::cout << "
";

    // โ”€โ”€โ”€ String views โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    std::vector<std::string> words{"hello","ranges","are","amazing","cpp20","is","great"};
    auto longUpperWords = words
        | std::views::filter([](const std::string& s){ return s.length() > 3; })
        | std::views::transform([](const std::string& s){
              std::string upper = s;
              std::ranges::transform(upper, upper.begin(), ::toupper);
              return upper;
          });
    std::cout << "Long words uppercase: ";
    for (const auto& w : longUpperWords) std::cout << w << " ";
    std::cout << "
";
    return 0;
}
3Materializing Views & Collecting Results
C++ โ€” Collecting view results into containersโ–ถ Run in Compiler
#include <iostream>
#include <ranges>
#include <vector>
#include <string>
#include <algorithm>

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

    // Materialize into vector using ranges::to (C++23)
    // In C++20: use ranges::copy + back_inserter
    auto evens = numbers | std::views::filter([](int n){ return n%2==0; });

    std::vector<int> evenVec;
    std::ranges::copy(evens, std::back_inserter(evenVec));
    std::cout << "Materialized evens: ";
    for (int n : evenVec) std::cout << n << " ";
    std::cout << "
";

    // Collect with transform into string
    std::string result;
    for (int n : numbers | std::views::filter([](int n){ return n>5; }))
        result += std::to_string(n) + " ";
    std::cout << "Greater than 5: " << result << "
";

    // ranges::for_each with projection
    std::vector<std::string> names{"charlie","alice","bob","diana"};
    std::ranges::for_each(names, [](const std::string& n){
        std::cout << n << " ";
    });
    std::cout << "
";

    // ranges::transform with back_inserter
    std::vector<std::string> upperNames;
    std::ranges::transform(names, std::back_inserter(upperNames),
                           [](std::string s) {
                               std::ranges::transform(s, s.begin(), ::toupper);
                               return s;
                           });
    std::cout << "Uppercase: ";
    for (const auto& n : upperNames) std::cout << n << " ";
    std::cout << "
";

    // Check with ranges predicates
    bool allPositive = std::ranges::all_of(numbers, [](int n){ return n>0; });
    bool anyOver8    = std::ranges::any_of(numbers, [](int n){ return n>8; });
    bool noneNeg     = std::ranges::none_of(numbers, [](int n){ return n<0; });
    std::cout << std::boolalpha
              << "all positive: " << allPositive
              << " any>8: " << anyOver8
              << " none negative: " << noneNeg << "
";
    return 0;
}
4View Adaptors Reference Table
View AdaptorDescriptionExample
views::filter(pred)Keep elements matching predicatev | views::filter(isEven)
views::transform(fn)Map each elementv | views::transform(square)
views::take(n)First n elementsv | views::take(5)
views::drop(n)Skip first n elementsv | views::drop(3)
views::take_while(pred)Take while predicate holdsv | views::take_while(lt10)
views::drop_while(pred)Skip while predicate holdsv | views::drop_while(lt5)
views::reverseReverse iteration orderv | views::reverse
views::iota(a, b)Sequence [a, b)views::iota(1, 100)
views::iota(a)Infinite sequence from aviews::iota(0) | views::take(10)
views::keysKeys of pair/map rangemyMap | views::keys
views::valuesValues of pair/map rangemyMap | views::values
views::elements<N>Nth element of tuple rangev | views::elements<2>
views::zip(r1, r2)Zip two ranges (C++23)views::zip(names, scores)
views::enumerateIndex + element pairs (C++23)v | views::enumerate
views::joinFlatten nested rangesvv | views::join
views::split(delim)Split by delimiterstr | views::split('/')
5Technical FAQs

Q1: Are views lazy or eager?

Views are completely lazy. No computation happens when you create a view pipeline. Elements are computed one-by-one on demand as you iterate. This means views::iota(0) | views::filter(pred) | views::take(5) only evaluates until 5 elements are found โ€” even from an infinite range.

Q2: Do views own their data?

No โ€” views are non-owning. They hold references/iterators into the original range. The original container must outlive the view. Never store a view to a local container that goes out of scope โ€” dangling reference!

Q3: What is a projection in ranges algorithms?

Projections let you specify a transformation to apply to elements before comparison: ranges::sort(people, {}, &Person::age) sorts by age without writing a comparator lambda. Works with member pointers and lambdas.

Q4: Can I collect a view into a container?

In C++20: std::ranges::copy(view, std::back_inserter(vec));. In C++23: auto vec = view | std::ranges::to<std::vector>(); โ€” much more concise. The to<> function materializes the lazy view into the target container.

Q5: What is the difference between std::ranges::sort and std::sort?

std::ranges::sort(v) accepts a single range โ€” impossible to pass mismatched iterators. std::sort(v.begin(), v.end()) requires an iterator pair. Both are O(n log n). Ranges version supports projections. Both have the same underlying algorithm.