Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a telemetry system that processed thousands of sensor readings per second. The developer had written a sequence of operations: filter out the noise, scale the values, and then take the top ten results. In C++17, this meant creating several temporary std::vector objects to hold the intermediate results, or writing a nested loop that was practically unreadable. It was the classic "iterator soup"—vec.begin(), vec.end() repeated six times in ten lines of code. It worked, but it was brittle and cluttered. When C++20 Ranges arrived, I showed them how to rewrite that entire block into a single, declarative pipeline. The look on their face was pure relief.

Moving Beyond Iterator Pairs

For decades, the C++ Standard Template Library (STL) forced us to think in terms of pairs of iterators. If you wanted to sort a vector, you didn't just sort the vector; you sorted the range defined by begin() and end(). It's redundant. A range is simply something that provides a beginning and an end, and C++20 finally treats that as a first-class citizen. Now, we can pass the container itself directly into algorithms.

But the real power isn't just in shorter function calls; it's in views. A view is a range that is lazily evaluated. When you apply a filter or a transformation to a range, C++ doesn't actually move any data or create a new container. It creates a lightweight object that knows how to apply those rules on the fly as you iterate through it. This means you can chain five different operations together and the actual work only happens when you finally pull a value out of the pipe.

Composing Logic with the Pipe Operator

The most striking part of the Ranges library is the pipe operator (|), borrowed heavily from functional programming and Unix shells. Instead of nesting function calls like transform(filter(data, predicate), mapper), which reads from the inside out, you can chain them linearly.

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

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

    auto result = numbers 
        | std::views::filter([](int n) { return n % 2 == 0; }) 
        | std::views::transform([](int n) { return n * n; })
        | std::views::take(3);

    for (int n : result) {
        std::cout << n << " "; // Outputs: 4 16 36
    }
}

Notice that result isn't a std::vector. It's a view. If the original numbers vector changes, the view reflects those changes because it doesn't own the data—it just knows how to navigate it. I usually recommend using this pattern whenever you're doing multi-stage data transformation, as it separates the what (filtering, transforming) from the how (the loop).

When to Materialize the Range

Because views are lazy, they can sometimes be tricky. If you need to pass the result to an API that expects a std::vector, or if you're going to iterate over the same processed data many times and don't want to re-calculate the transformation every time, you need to "materialize" the range.

In C++23, we got std::ranges::to<std::vector>(), which is the gold standard for this. In C++20, you'll often find yourself initializing a vector using the range's iterators or using a simple loop to populate a container. Just keep in mind: if you're just printing values or passing them to another view, stay lazy. Only pay the cost of allocation and copying when you actually need to own the resulting data.




📋 Practical Task

Refactoring the System Log Parser

You have been handed a piece of legacy code that parses system logs. The current implementation uses a clunky loop to find all "ERROR" entries and convert them to uppercase. Your task is to refactor this using C++20 Ranges.

Requirements:

  • Start with a std::vector<std::string> containing various log levels (e.g., "INFO: System boot", "ERROR: Disk full", "DEBUG: Cache hit", "ERROR: Network timeout").
  • Use std::views::filter to keep only the strings that start with "ERROR".
  • Use std::views::transform to strip the "ERROR: " prefix from the remaining strings.
  • Use std::views::take to only process the first 2 errors found.
  • Print the resulting strings to the console.
// Starter code
#include <iostream>
#include <vector>
#include <string>
#include <ranges>

int main() {
    std::vector<std::string> logs = {
        "INFO: System boot",
        "ERROR: Disk full",
        "DEBUG: Cache hit",
        "ERROR: Network timeout",
        "ERROR: Kernel panic",
        "INFO: User logged in"
    };

    // TODO: Implement the range pipeline here
    
    return 0;
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.