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
244: Range Algorithms vs Classic Algorithms
I've noticed a recurring trend when developers move from C++17 to C++20: they treat the new std::ranges namespace as nothing more than a "convenience wrapper." They assume that std::ranges::sort(vec) is just a shorthand for std::sort(vec.begin(), vec.end()) and that there isn't any real architectural reason to prefer one over the other.
If that were true, Ranges would be a minor quality-of-life update. But it's not. When you treat them as mere syntactic sugar, you miss the most powerful part of the library: projections and lazy evaluation. To see why the "just sugar" mindset is wrong, look at how we used to handle a list of custom objects.
Stop writing boilerplate comparators
Imagine you have a User struct and you want to sort a vector of users by their ID. In the classic world, you're forced to write a lambda that describes how to compare two User objects. It looks like this:
std::sort(users.begin(), users.end(), [](const User& a, const User& b) {
return a.id < b.id;
});
It's not terrible, but it's repetitive. You're manually extracting the member you care about and then applying the comparison. Now, look at the Ranges version. I can pass the member pointer as a projection:
std::ranges::sort(users, {}, &User::id);
That second argument (the {}) tells C++ to use the default comparator (less-than), and the third argument tells it: "Before you compare these two objects, project them down to their id first." This is a massive win for readability. I'm no longer describing how to compare two users; I'm telling the algorithm what attribute to sort by.
Composition over temporary containers
The second big shift is the move from "eager" algorithms to "lazy" views. In the classic STL, if you wanted to filter a list of numbers and then square the results, you usually had to create a temporary vector to hold the filtered results before passing them to another algorithm. It's a lot of memory allocation for something that should be a simple pipeline.
With std::views, we can compose operations using the pipe operator (|). I personally love this because it reads like a data pipeline in a functional language, but it performs with C++ efficiency.
auto result = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
Here is the critical distinction: result is not a new vector. It's a view. No filtering or squaring has actually happened yet. The work is deferred until you actually iterate over result in a loop. If you only ever read the first two elements of that result, the computer never bothers to process the rest of the list. You cannot do that with classic algorithms without writing your own custom iterator logic from scratch.
When to stick with the classics
You might be wondering if you should delete all your .begin() and .end() calls. Not quite. Classic algorithms are still the right tool when you are working with legacy codebases or when you specifically need to operate on a sub-range that isn't easily defined by a view. However, for 90% of modern C++ development, if you can use a range, you should. It's safer, more concise, and significantly more flexible.
📋 Practical Task
Build a Product Price Filter Pipeline
You are building a simplified inventory system. You have a Product struct with a name (string) and a price (double). Your goal is to create a pipeline that processes a std::vector<Product> and produces a view of the names of all products that cost more than $50.00.
Requirements:
- Define a
Productstruct. - Create a vector containing at least five products with varying prices.
- Use
std::views::filterto keep only products with a price > 50.0. - Use
std::views::transformto extract only thenamefrom the filtered products. - Print the resulting names using a
forloop. - Constraint: You must use the pipe operator (
|) and must not create any intermediate vectors to store the filtered results.
There are no comments for now.