C++20 introduced the Ranges library, which provides composable, lazy views for transforming and filtering data without creating intermediate containers. Views are lightweight wrappers that compute elements on-demand — no allocation, no copying. By combining (composing) multiple views, you can build complex data pipelines that process elements only when iterated, leading to significant performance and memory improvements.
A view does not execute its transformation when created. It stores the recipe and applies it only when you iterate:
1#include <ranges>
2#include <vector>
3#include <iostream>
4
5std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
6
7// No computation happens here — just builds the pipeline
8auto pipeline = nums
9| std::views::filter([](int n) { return n % 2 == 0; }) | std::views::transform([](int n) { return n * n; }); // Computation happens HERE, element by element for (int val : pipeline) { std::cout << val << " "; // 4 16 36 64 100 } ``` No intermediate vector of even numbers is created. Each element flows through the full pipeline one at a time. ## Core Views in C++20 ### Filtering ```cpp auto evens = nums | std::views::filter([](int n) { return n % 2 == 0; }); // Yields: 2, 4, 6, 8, 10 ``` ### Transforming ```cpp auto squares = nums | std::views::transform([](int n) { return n * n; }); // Yields: 1, 4, 9, 16, 25, 36, 49, 64, 81, 100 ``` ### Taking and Dropping ```cpp auto first3 = nums | std::views::take(3); // 1, 2, 3 auto skip3 = nums | std::views::drop(3); // 4, 5, 6, 7, 8, 9, 10 auto while_small = nums | std::views::take_while([](int n) { return n < 5; }); // 1, 2, 3, 4 ``` ### Reversing ```cpp auto reversed = nums | std::views::reverse; // 10, 9, 8, ..., 1 ``` ### Iota (Generate Sequences) ```cpp // Infinite sequence starting at 0 auto naturals = std::views::iota(0); // First 10 even numbers auto first10even = std::views::iota(0) | std::views::filter([](int n) { return n % 2 == 0; }) | std::views::take(10); // 0, 2, 4, 6, 8, 10, 12, 14, 16, 18 ``` ## Composing Multiple Views The pipe operator (`|`) chains views into complex lazy pipelines: ```cpp #include <ranges> #include <vector> #include <string> struct Employee { std::string name; int department; double salary; }; std::vector<Employee> employees = { /* ... */ }; // Find names of top-paid engineers auto top_engineers = employees | std::views::filter([](const Employee& e) { return e.department == 42; // engineering dept }) | std::views::filter([](const Employee& e) { return e.salary > 100000; // high salary }) | std::views::transform([](const Employee& e) { return e.name; // extract name }) | std::views::take(5); // limit to 5 for (const auto& name : top_engineers) { std::cout << name << "\n"; } ``` This processes each employee through all stages before moving to the next — no intermediate vectors are allocated. ## Benefits of Lazy Composition * **Performance Improvement**: Because operations are deferred, elements are processed only when they are accessed. No wasted computation on elements you never use. * **Reduced Memory Footprint**: Since operations are performed on-the-fly, memory allocation for intermediate results is minimized. A pipeline over 1 million elements uses O(1) extra memory. * **Enhanced Composability**: By combining lazy operations, complex transformations can be built from simple, reusable parts. * **Short-circuit evaluation**: `take(5)` stops the entire pipeline after 5 matches — upstream filters/transforms are never applied to remaining elements. ## C++23 Additions C++23 adds more view combinators: ```cpp // std::views::zip — combine multiple ranges auto zipped = std::views::zip(names, ages); // Yields pairs: ("Alice", 30), ("Bob", 25), ... // std::views::enumerate — index + element for (auto [i, val] : nums | std::views::enumerate) { std::cout << i << ": " << val << "\n"; } // std::views::chunk — split into groups auto pairs = nums | std::views::chunk(2); // {1,2}, {3,4}, {5,6}, ... // std::views::slide — sliding window auto windows = nums | std::views::slide(3); // {1,2,3}, {2,3,4}, {3,4,5}, ... ``` ## Materializing Views into Containers Views are lazy — to store results, convert to a container: ```cpp // C++23: std::ranges::to auto result = pipeline | std::ranges::to<std::vector>(); // C++20: manual construction auto view = nums | std::views::filter([](int n) { return n > 5; }); std::vector<int> result(view.begin(), view.end()); ``` ## Common Pitfalls * **Dangling references**: Views do not own data. If the underlying container is destroyed or moved, the view becomes dangling. Ensure the source outlives the view. * **Multiple iterations**: Some views (like `filter`) recompute on each iteration. If the predicate is expensive, cache results by materializing to a container after the first pass. * **Not all views are bidirectional**: `filter` views are forward-only because the predicate must be evaluated sequentially. You cannot use `views::reverse` after `views::filter`. * **Compilation errors**: Template-heavy ranges code produces notoriously verbose error messages. Start with simple pipelines and add stages one at a time. * **Compiler support**: Full C++20 ranges support requires GCC 10+, Clang 15+, or MSVC 19.29+. C++23 features require newer compilers (GCC 13+, Clang 17+). ## Summary * C++20 views are lazy — they compute elements on-demand without allocating intermediate containers * Use the pipe operator (`|`) to compose `filter`, `transform`, `take`, `drop`, and other views * Lazy pipelines use O(1) extra memory and short-circuit when possible * Views do not own data — ensure the source container outlives the view * C++23 adds `zip`, `enumerate`, `chunk`, `slide`, and `std::ranges::to` for materialization