Skip to Content
Course content

246: std::function and Type Erasure

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 Command type using std::function that matches the signature: bool(int).
  • Create a std::vector to act as the commandQueue.
  • Implement three different types of commands to add to the queue:
    1. A regular function called moveEntity that prints "Moving entity [ID]" and returns true.
    2. A lambda called attackEntity that captures a "damage" variable from the local scope, prints "Attacking entity [ID] for [damage] damage", and returns true.
    3. A lambda called healEntity that prints "Healing entity [ID]" and returns false (simulating a failed heal).
  • Iterate through the queue, execute each command with a dummy Entity ID (e.g., 42), and print whether the command succeeded or failed.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.