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
72: Custom Comparators and Functors
You've probably already used std::sort with a vector of integers or strings, and it just works. That's because the C++ Standard Library knows exactly how to compare those types. But once you start building real-world objects—say, a Player struct for a game—you hit a wall. You can't just tell the compiler to "sort the players," because the compiler doesn't know if you care about their high score, their username, or their join date.
The Trap of the Single Operator
The first instinct for most developers is to overload the operator< inside the class. It feels intuitive. You define what "less than" means for a Player, and suddenly std::sort works again. I've seen this in countless code reviews, and while it's not "wrong" in a technical sense, it's architecturally rigid.
struct Player {
std::string name;
int score;
// The naive approach: Hardcoding the sort order
bool operator<(const Player& other) const {
return score < other.score;
}
};
Here is the problem: you've just locked your Player class into one single sorting identity. What happens when you need a leaderboard sorted by score (descending), but a social list sorted alphabetically by name? You can't overload operator< three different times for three different use cases. You're stuck. You've told the system that a Player is their score, which is rarely true in a complex application.
Adding Flexibility with Function Objects
This is where functors—or "function objects"—come in. A functor is simply a class or struct that overloads the operator(). It allows you to treat an object as if it were a function. Instead of baking the comparison logic into the data model, you move it into a separate, specialized tool that you pass to the algorithm.
struct CompareByName {
bool operator()(const Player& a, const Player& b) const {
return a.name < b.name;
}
};
struct CompareByScore {
bool operator()(const Player& a, const Player& b) const {
return a.score > b.score; // Higher score comes first
}
};
// Usage:
std::sort(players.begin(), players.end(), CompareByName());
std::sort(players.begin(), players.end(), CompareByScore());
Now your Player struct is just a data container again, and your sorting logic is modular. If you need a new way to sort, you just write a new functor. You aren't modifying existing, tested code to add a new feature; you're extending the system.
Why not just use a regular function?
You might be wondering why we bother with a struct and operator() when a simple boolean function would do the same thing. In the old days of C, you'd pass a function pointer. But in C++, functors have a massive performance advantage: inlining.
When you pass a function pointer to std::sort, the compiler often has to jump to a different memory address for every single comparison, which kills your instruction cache. With a functor, the compiler knows the exact type of the comparator at compile time. It can literally bake the comparison logic directly into the sorting loop, often resulting in a significant speed boost.
Beyond speed, functors can hold state. Imagine a comparator where you can pass a boolean to the constructor to toggle between ascending and descending order. A regular function can't do that without relying on ugly global variables. A functor just stores that preference as a member variable, keeping everything encapsulated and thread-safe.
📋 Practical Task
Implementing a Multi-Criteria Tournament Ranker
You are building a tournament system where players are ranked primarily by their wins. However, if two players have the same number of wins, the tie-breaker is their averageMatchTime (where a lower time is better). Finally, if they are still tied, they are sorted alphabetically by playerName.
Your Task:
- Create a
Playerstruct withstd::string playerName,int wins, anddouble averageMatchTime. - Implement a functor called
TournamentComparatorthat implements the three-tier sorting logic described above. - In your
mainfunction, create astd::vectorof at least five players, including at least two players who have the same number of wins but different match times, and two who have the same wins AND match times but different names. - Use
std::sortwith yourTournamentComparatorto order the list and print the results to the console to verify the tie-breaking logic works correctly.
There are no comments for now.