C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
243: Range Adaptors and Composability
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:
- Filters the items to keep only those with a rarity of "Legendary".
- Filters those remaining items to keep only those with a
powerLevelgreater than 50. - Transforms the remaining items into a string format:
"[Item Name] (Power: X)". - 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.
There are no comments for now.