Skip to Content
Course content

243: Range Adaptors and Composability

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

Up until now, we've talked about ranges as a way to handle sequences of data. But the real magic of C++20 ranges isn't just in the ranges themselves—it's in the adaptors. If ranges are the "what," adaptors are the "how we reshape it." The beauty here is composability. Instead of nesting function calls like a set of Russian dolls, we use the pipe operator | to build a data pipeline.

Let's build something practical. Imagine we're writing a piece of a game engine. We have a list of Player objects, and we want to generate a "Hall of Fame" list: only players with a score over 1,000, sorted by their score, but we only want the names of the top 3.

Setting up the player data

First, I'll define a simple struct. I'm keeping it lean because the focus here is the pipeline, not the data model.

struct Player {
    std::string name;
    int score;
};

std::vector<Player> players = {
    {"Alice", 1200}, {"Bob", 800}, {"Charlie", 1500}, 
    {"Dave", 700}, {"Eve", 2000}, {"Frank", 1100}
};

Building the processing pipeline

I want to filter out the low scorers and then extract just the names. In the old days, I'd have created a temporary vector, looped through the original, pushed back the matches, and then looped again to get the names. That's a lot of boilerplate and unnecessary memory allocation.

With range adaptors, I can just pipe the data. I'll use std::views::filter to keep the high scores and std::views::transform to turn a Player object into a std::string.

auto high_score_names = players 
    | std::views::filter([](const auto& p) { return p.score > 1000; })
    | std::views::transform([](const auto& p) { return p.name; });

The "Wait, why is my list too short?" mistake

Now, I only want the top 3. I'll add std::views::take(3) to the end. But watch what happens if I get my logic order wrong. I'm going to try and "optimize" by taking the first 3 players before I filter them, thinking I can limit the workload.

// INCORRECT LOGIC
auto mistake = players 
    | std::views::take(3) 
    | std::views::filter([](const auto& p) { return p.score > 1000; });

I ran this and only got Alice and Charlie. Why? Because take(3) grabbed Alice, Bob, and Charlie. Then, the filter looked at those three and threw Bob out because his score was 800. I ended up with 2 players instead of 3. This is a classic "composition order" bug. The pipe operator processes from left to right; the output of one adaptor is the input for the next.

To fix this, I need to filter the entire list first, and then take the top 3 from the surviving candidates.

// CORRECT LOGIC
auto final_list = players 
    | std::views::filter([](const auto& p) { return p.score > 1000; })
    | std::views::transform([](const auto& p) { return p.name; })
    | std::views::take(3);

The power of laziness

Here is the part that always trips people up: final_list isn't actually a list. It's a view. If you look at the assembly or debug the code, you'll notice that none of the filtering or transforming actually happened when I defined final_list.

The work only happens when I actually iterate over it. This is called lazy evaluation. It's incredibly efficient because if I only ever read the first element of final_list, the computer doesn't waste time filtering the rest of the 1,000-player vector.

for (const std::string& name : final_list) {
    std::cout < < name < < "\n";
}



📋 Practical Task

Build a Legendary Item Search Filter

You are implementing a search feature for an RPG inventory system. You have a std::vector of Item structs, where each item has a std::string name, an int powerLevel, and a std::string rarity (e.g., "Common", "Rare", "Legendary").

Your task is to create a range pipeline that does the following in this exact order:

  1. Filters the items to keep only those with a rarity of "Legendary".
  2. Filters those remaining items to keep only those with a powerLevel greater than 50.
  3. Transforms the remaining items into a string format: "[Item Name] (Power: X)".
  4. Limits the final result to the first 5 items found.

Write the code to define this pipeline and a for loop to print the results to the console. Make sure your pipeline is composed using the pipe | operator.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.