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
246: std::function and Type Erasure
I've seen this happen a dozen times with developers moving from languages like C# or Java into C++. They try to build a simple callback system—maybe for a UI button or a game event—and they hit a wall the moment they try to store those callbacks in a list. Let's look at a piece of code that looks perfectly logical on the surface but will make the compiler lose its mind.
#include <iostream>
#include <vector>
void logMessage() { std::cout < "Log triggered!\n"; }
int main() {
auto onSave = []() { std::cout < "Saving file...\n"; };
auto onQuit = []() { std::cout < "Quitting app...\n"; };
// I want a list of things to do when the app closes
// This is where it all falls apart:
std::vector<decltype(onSave)> shutdownTasks = { onSave, onQuit, logMessage };
for (auto& task : shutdownTasks) {
task();
}
}
The Lambda Type Nightmare
If you try to compile this, you'll get a wall of errors. You might think, "But they all take no arguments and return nothing! Why does the vector care?"
Here is the catch: in C++, every single lambda expression has a unique, unnamed type generated by the compiler. Even if two lambdas have the exact same signature (same inputs and outputs), they are fundamentally different types. To the compiler, onSave and onQuit are as different as an int and a std::string. Then you've got logMessage, which is a regular function pointer. You cannot put these three different types into a std::vector because a vector requires a single, concrete type for all its elements.
Erasing Types with std::function
This is where std::function comes in. It's a "type-erasing" wrapper. When we say "type erasure," we mean that std::function hides the specific concrete type of the callable (whether it's a lambda, a function pointer, or a functor) and only remembers the signature.
By replacing the rigid decltype(onSave) with std::function<void()>, we're telling the compiler: "I don't care what the actual object is, as long as it can be called like a function that returns void and takes no arguments."
#include <iostream>
#include <vector>
#include <functional> // Required for std::function
void logMessage() { std::cout < "Log triggered!\n"; }
int main() {
auto onSave = []() { std::cout < "Saving file...\n"; };
auto onQuit = []() { std::cout < "Quitting app...\n"; };
// Now we store the 'signature', not the 'concrete type'
std::vector<std::function<void()>> shutdownTasks;
shutdownTasks.push_back(onSave);
shutdownTasks.push_back(onQuit);
shutdownTasks.push_back(logMessage);
for (auto& task : shutdownTasks) {
task(); // The wrapper handles the call to the underlying type
}
}
The Performance Trade-off
Now, as your mentor, I have to give you the caveat. Type erasure isn't free. Because std::function has to wrap various types of callables, it often uses dynamic memory allocation (the heap) to store the lambda's capture block and uses a virtual-call-like mechanism to execute the code.
For 95% of your application logic, you won't notice the difference. But if you're writing a tight loop that executes a callback a million times per second in a physics engine, std::function might be too slow. In those rare cases, you'd look at templates or custom polymorphism, but for general-purpose event systems, std::function is the industry standard for a reason: it's incredibly flexible.
📋 Practical Task
Building a Flexible Game Command Queue
Your task is to create a simple "Command Queue" system for a game. Instead of just void(), this system needs to handle commands that take an integer (representing the 'Entity ID' being acted upon) and return a boolean (representing whether the command succeeded).
Requirements:
- Define a
Commandtype usingstd::functionthat matches the signature:bool(int). - Create a
std::vectorto act as thecommandQueue. - Implement three different types of commands to add to the queue:
- A regular function called
moveEntitythat prints "Moving entity [ID]" and returnstrue. - A lambda called
attackEntitythat captures a "damage" variable from the local scope, prints "Attacking entity [ID] for [damage] damage", and returnstrue. - A lambda called
healEntitythat prints "Healing entity [ID]" and returnsfalse(simulating a failed heal).
- A regular function called
- Iterate through the queue, execute each command with a dummy Entity ID (e.g., 42), and print whether the command succeeded or failed.
There are no comments for now.