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
90: Ranges (C++20)
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::filterto keep only the strings that start with "ERROR". - Use
std::views::transformto strip the "ERROR: " prefix from the remaining strings. - Use
std::views::taketo 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;
}
There are no comments for now.