Ranges and views

C++ · Advanced · 15 min

Ranges make data pipelines shorter and safer than iterator pairs, and views avoid building temporary containers.

C++20's <ranges> brings two big improvements.

Range algorithms. Everything in std::ranges:: accepts a whole container: std::ranges::sort(v) instead of std::sort(v.begin(), v.end()). They're constrained with concepts, so misuse produces readable errors, and they accept projections, a function applied to each element before comparing. std::ranges::sort(staff, {}, &Employee::age) sorts by age with no lambda; std::ranges::find(staff, "Mo", &Employee::name) finds by name.

Views. A view is a lightweight, lazy range adaptor that doesn't own elements and computes them on demand. Compose them with |:

  • views::filter(pred), views::transform(f), views::take(n), views::drop(n), views::reverse
  • views::iota(a, b) generates numbers; views::iota(1) counts forever (fine when combined with take)
  • views::keys / views::values on maps, views::split, views::join

Nothing runs until you iterate, and each element flows through the whole pipeline individually, with no intermediate vectors. C++23 adds views::zip, views::enumerate, views::chunk, views::slide, and std::ranges::to<std::vector>() to collect a view into a container.

Things to watch:

  • Views refer to their underlying data. A view over a container must not outlive it, just like an iterator.
  • Some views (such as filter) cache their begin() and must be non-const to iterate; pass views to functions by value or forwarding reference rather than const&.
  • Range algorithms called on a temporary container return std::ranges::dangling instead of an iterator, so the compiler stops you from using a dangling result.

Example

#include <algorithm>
#include <iostream>
#include <map>
#include <ranges>
#include <string>
#include <vector>

struct Employee { std::string name; int age; };

int main() {
    std::vector<Employee> staff{{"Zoe", 41}, {"Al", 29}, {"Mo", 35}};
    std::ranges::sort(staff, {}, &Employee::age);       // whole range + projection
    std::cout << staff.front().name << '\n';           // Al

    namespace views = std::views;
    auto squares_of_odds = views::iota(1)              // 1, 2, 3, ... lazily
        | views::filter([](int n) { return n % 2 == 1; })
        | views::transform([](int n) { return n * n; })
        | views::take(4);
    for (int x : squares_of_odds) std::cout << x << ' ';   // 1 9 25 49
    std::cout << '\n';

    std::map<std::string, int> stock{{"apple", 3}, {"kiwi", 0}};
    for (const auto& name : stock | views::keys) std::cout << name << ' ';  // apple kiwi
    std::cout << '\n';

    auto it = std::ranges::find(staff, "Mo", &Employee::name);   // projection again
    std::cout << it->age << '\n';                      // 35
    // C++23: auto v = squares_of_odds | std::ranges::to<std::vector>();
}

Remember

  • std::ranges:: algorithms take whole ranges and support projections
  • Views are lazy and non-owning; compose them with |
  • A view must not outlive the data it looks at

Watch out for

  • Returning a view over a local container from a function — the view dangles once the container is destroyed.

Check yourself

  1. When does the filter lambda in `auto v = nums | std::views::filter(pred);` run?
  2. What does `std::ranges::sort(people, {}, &Person::height)` do?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: en.cppreference.com/w/cpp/ranges

Builds on

Unlocks

Nothing builds on this yet.