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
84: Lambda Expressions
I was working on a small project the other day—a simple top-down shooter—and I ran into a classic C++ annoyance. I had a list of enemies in a std::vector, and I wanted to remove all the ones that had their health drop to zero. Now, the "correct" way to do this in C++ is the erase-remove idiom using std::remove_if.
The Boilerplate Burden
At first, I did what I always did: I wrote a separate predicate function to handle the logic. It looked something like this:
struct Enemy {
std::string name;
float health;
};
bool isDead(const Enemy& e) {
return e.health <= 0.0f;
}
// ... inside main ...
std::vector<Enemy> enemies = { {"Goblin", 10.0f}, {"Orc", 0.0f}, {"Troll", 5.0f} };
enemies.erase(std::remove_if(enemies.begin(), enemies.end(), isDead), enemies.end());
It works, sure. But as I looked at my code, I realized I was defining a function in the global namespace (or as a static member) that is used exactly once in the entire program. It's a bit of clutter. I found myself thinking, "Why can't I just put the logic right where I'm calling the function?"
Moving the Logic Inline
This is where lambda expressions come in. I decided to scrap the isDead function and try the lambda syntax. A lambda is essentially an anonymous function you define on the fly. I replaced isDead with this block:
enemies.erase(
std::remove_if(enemies.begin(), enemies.end(), [](const Enemy& e) {
return e.health <= 0.0f;
}),
enemies.end()
);
If you've seen these before, the [] is the "capture clause," the (const Enemy& e) is the parameter list, and the {...} is the body. It's much cleaner. The logic is now physically located where it's being used, so I don't have to scroll up fifty lines to remember what isDead actually does.
The "Scope Wall" Problem
Now, here is where things got interesting. I decided I didn't want a hardcoded 0.0f. I wanted a variable, minHealth, that I could tweak during gameplay to simulate a "mercy" mechanic where enemies only die if they hit a specific threshold.
I tried this:
float minHealth = -5.0f;
enemies.erase(
std::remove_if(enemies.begin(), enemies.end(), [](const Enemy& e) {
return e.health <= minHealth; // Error!
}),
enemies.end()
);
The compiler immediately complained. It told me minHealth wasn't captured. This is the "scope wall." A lambda is its own little island; it can't see the local variables of the function it's sitting in unless you explicitly invite them in via that [] bracket.
Breaking Through with Captures
To fix this, I had to tell the lambda to "capture" minHealth. I tried adding it to the brackets:
enemies.erase(
std::remove_if(enemies.begin(), enemies.end(), [minHealth](const Enemy& e) {
return e.health <= minHealth;
}),
enemies.end()
);
That worked. By putting minHealth in the brackets, I created a copy of that variable inside the lambda. But what if I wanted the lambda to actually change the variable? Or what if I had ten different variables and didn't want to list them all?
I experimented with two shorthand options. Using [=] tells C++, "capture everything I use by value (make copies)." Using [&] tells it, "capture everything I use by reference."
I'll give you a word of caution here: [&] is incredibly convenient, but it's dangerous if the lambda outlives the variables it captures (like if you pass the lambda to another thread or store it in a class). In this case, since std::remove_if executes immediately, [&] is perfectly safe and slightly more performant since we aren't copying data.
My final, polished version ended up looking like this:
float minHealth = -5.0f;
enemies.erase(
std::remove_if(enemies.begin(), enemies.end(), [&](const Enemy& e) {
return e.health <= minHealth;
}),
enemies.end()
);
It's concise, it's local, and it's powerful. We've turned a multi-line boilerplate process into a single, readable expression.
📋 Practical Task
Implementing a Custom Sort for a High-Score Leaderboard
You are building a leaderboard for a game. You have a struct Player with a std::string name and an int score. You have a std::vector<Player> containing several players.
Your task is to use std::sort from the <algorithm> header and a lambda expression to sort the players based on the following rules:
- Players should be sorted by
scorein descending order (highest score first). - If two players have the same score, they should be sorted by
namein alphabetical order.
Write a small program that initializes a vector with at least four players (including at least two with the same score) and prints the sorted list to the console to verify your lambda logic works correctly.
There are no comments for now.