EduC++ / C++20 Ranges and Views

C++20 Ranges and Views

Prereqs Iterators, lambdas, STL algorithm basics
Standard C++20 (headers <algorithm>, <ranges>)

Frequently Asked Questions

QCan views be used with plain C arrays?
AYes. C arrays model the contiguous_range concept, so most view adaptors (filter, transform, take, drop) work directly on them. You can also wrap a C array with std::span for added safety without copying.
QAre views always lazy?
AAll standard view adaptors are lazy -- they compute elements on demand when iterated. However, some adaptors (e.g., views::reverse on a non-bidirectional range, or views::join in certain cases) may cache iterators internally for performance, which can consume a small amount of state. The key guarantee is that no intermediate container is created.
QHow do I materialize a view into a concrete container?
AIn C++23, use std::ranges::to<std::vector>() at the end of a pipeline. In C++20, construct the container from the view's begin/end iterators: auto v = std::vector(view.begin(), view.end()); or use std::ranges::copy into a back_inserter.
QHow does the ranges library protect against dangling iterators?
AWhen you pass an rvalue (temporary) range to a ranges algorithm that returns an iterator, the library returns the special type std::ranges::dangling instead of an actual iterator. This causes a compile error if you try to dereference it, preventing use-after- destruction bugs that are common with classic STL algorithms.
C++
#include <iostream>
#include <format>
#include <vector>
#include <string>
#include <algorithm>
#include <ranges>

C++
int main() {

1 Range-based algorithms (no begin/end needed!)

What

Ranges compose algorithms and views into lazy, pipeline-style transformations.

When

Use them for readable data-processing chains over containers and iterables.

Why

They reduce temporary containers and improve expression-level clarity.

Use

Compose with `|` and call range algorithms on views/containers.

C++ Version C++20

std::ranges::sort replaces std::sort(v.begin(), v.end())

std:: algorithms are different overloads. Do not mix their iterator types in one call.

Watch out: std::ranges:: algorithms and

C++
std::cout << "--- Range Algorithms ---\n";
    std::vector<int> nums = {5, 2, 8, 1, 9, 3, 7};

    std::ranges::sort(nums);  // Clean! No begin/end
    std::cout << "Sorted: ";
    for (int n : nums) std::cout << n << ' ';
    std::cout << '\n';

    // ranges::find returns a sentinel-aware iterator
    if (auto it = std::ranges::find(nums, 7); it != nums.end()) {
        std::cout << std::format("Found 7 at index {}\n",
                                  std::distance(nums.begin(), it));
    }

    // ranges::count, ranges::min, ranges::max
    std::cout << std::format("Min: {}, Max: {}\n",
                              std::ranges::min(nums),
                              std::ranges::max(nums));

2 Views: lazy, composable transformations

What

Views: lazy, composable transformations.

When

Use this when it cleanly solves the problem in front of you.

Why

Views don't modify the original data.

Use

Use it in code like: std::cout << "\n--- Views ---\n";.

C++ Version Not explicitly specified in this example.

Views don't modify the original data. They are evaluated on-demand (lazy).

C++
std::cout << "\n--- Views ---\n";

    // filter: keep only even numbers
    std::cout << "Even numbers: ";
    for (int n : nums | std::views::filter([](int n) { return n % 2 == 0; })) {
        std::cout << n << ' ';
    }
    std::cout << '\n';

    // transform: square each element
    std::cout << "Squared: ";
    for (int n : nums | std::views::transform([](int n) { return n * n; })) {
        std::cout << n << ' ';
    }
    std::cout << '\n';

    // take: only the first N elements
    std::cout << "First 3: ";
    for (int n : nums | std::views::take(3)) {
        std::cout << n << ' ';
    }
    std::cout << '\n';

    // drop: skip the first N elements
    std::cout << "Skip 4: ";
    for (int n : nums | std::views::drop(4)) {
        std::cout << n << ' ';
    }
    std::cout << '\n';

    // reverse
    std::cout << "Reversed: ";
    for (int n : nums | std::views::reverse) {
        std::cout << n << ' ';
    }
    std::cout << '\n';

3 Composing views with the pipe operator

What

Ranges compose algorithms and views into lazy, pipeline-style transformations.

When

Use them for readable data-processing chains over containers and iterables.

Why

They reduce temporary containers and improve expression-level clarity.

Use

Compose with `|` and call range algorithms on views/containers.

C++ Version C++20

This is the real power of ranges: chain multiple transformations, evaluated lazily.

C++
std::cout << "\n--- Composing Views ---\n";

    // Get squares of even numbers, take first 2
    auto pipeline = nums
        | std::views::filter([](int n) { return n % 2 == 0; })
        | std::views::transform([](int n) { return n * n; })
        | std::views::take(2);

    std::cout << "Squares of evens (first 2): ";
    for (int n : pipeline) {
        std::cout << n << ' ';
    }
    std::cout << '\n';

4 views::iota -- generate a range of numbers

What

Ranges compose algorithms and views into lazy, pipeline-style transformations.

When

Use them for readable data-processing chains over containers and iterables.

Why

They reduce temporary containers and improve expression-level clarity.

Use

Compose with `|` and call range algorithms on views/containers.

C++ Version C++20
C++
std::cout << "\n--- views::iota ---\n";

    // Numbers 1 to 10
    std::cout << "1..10: ";
    for (int n : std::views::iota(1, 11)) {
        std::cout << n << ' ';
    }
    std::cout << '\n';

    // FizzBuzz with ranges!
    std::cout << "\nFizzBuzz (1-15):\n";
    for (int n : std::views::iota(1, 16)) {
        if (n % 15 == 0)      std::cout << "FizzBuzz ";
        else if (n % 3 == 0)  std::cout << "Fizz ";
        else if (n % 5 == 0)  std::cout << "Buzz ";
        else                   std::cout << std::format("{} ", n);
    }
    std::cout << '\n';

5 Projections: transform the comparison key

What

A lambda defines an unnamed callable object directly at the use site.

When

Use lambdas for short callbacks, predicates, and local behavior.

Why

They keep behavior close to the call site and avoid extra named functor types.

Use

Write [captures](params) { body } and keep captured state explicit.

C++ Version C++11

Sort strings by length without a custom comparator.

Projections are a major ergonomic win over classic STL: instead of writing a lambda comparator, pass a member pointer or callable as the third argument to ranges:: algorithms.

C++
std::cout << "\n--- Projections ---\n";
    std::vector<std::string> words = {"banana", "fig", "cherry", "apple", "date"};

    // Sort by string length using a projection
    std::ranges::sort(words, {}, &std::string::size);

    std::cout << "Sorted by length: ";
    for (const auto& w : words) {
        std::cout << std::format("{}({}) ", w, w.size());
    }
    std::cout << '\n';

    return 0;
}