Menu
Coddy logo textTech

Type Erasure

Part of the Object Oriented Programming section of Coddy's C++ journey — lesson 79 of 104.

Type erasure is a technique that allows you to store objects of different types in a uniform way, hiding their concrete types behind a common interface. Unlike templates (which generate separate code for each type) or inheritance (which requires a common base class), type erasure lets unrelated types be treated uniformly at runtime.

The classic example is std::function, which can hold any callable - functions, lambdas, or functors - regardless of their actual type:

#include <functional>
#include <iostream>

void regularFunc(int x) { std::cout << x << "\n"; }

struct Functor {
    void operator()(int x) { std::cout << x * 2 << "\n"; }
};

int main() {
    std::function<void(int)> f;
    
    f = regularFunc;    // Holds a function pointer
    f(5);               // Output: 5
    
    f = Functor{};      // Holds a functor
    f(5);               // Output: 10
    
    f = [](int x) { std::cout << x + 1 << "\n"; };  // Holds a lambda
    f(5);               // Output: 6
}

Type erasure works by wrapping objects in an internal polymorphic hierarchy that the user never sees. The wrapper stores the object and forwards calls through virtual functions, "erasing" the original type while preserving the behavior.

This technique is powerful when you need containers of heterogeneous callable objects or want to decouple interfaces from implementations without forcing inheritance on client code.

challenge icon

Challenge

Easy

Let's build a flexible task executor that demonstrates type erasure by storing different callable types in a uniform container. You'll create a Task class that can hold any callable—functions, lambdas, or functors—and execute them through a common interface, similar to how std::function works internally.

You'll organize your code across three files:

  • Task.h: Define your type-erased Task class that can store and execute any callable that takes no parameters and returns an int.

    Your Task class needs an internal polymorphic hierarchy that the user never sees. Create a private abstract base class (like Concept) with a pure virtual invoke() method. Then create a template-derived class (like Model) that stores the actual callable and implements invoke() by calling it.

    The Task class should store a std::unique_ptr to the base class. Provide:

    • A template constructor that accepts any callable and wraps it in the appropriate Model
    • An execute() method that invokes the stored callable and returns its result

    This is the essence of type erasure: the Task class hides the concrete type of the callable behind the polymorphic interface.

  • Callables.h: Define some different callable types to demonstrate that your Task can hold them all.

    Create a regular function computeSquare() that returns 25 (5 squared).

    Create a functor class Multiplier that stores two integers and returns their product when called.

  • main.cpp: Read two integers from input (each on a separate line). These will be used to initialize the Multiplier functor.

    Create a std::vector<Task> and add three different tasks to it:

    1. A task wrapping the computeSquare function
    2. A task wrapping a Multiplier functor initialized with the two input values
    3. A task wrapping a lambda that returns 42

    Then iterate through the vector and execute each task, printing the results. For each task, print Task [index]: [result] where index starts at 1.

For example, with inputs 7 and 8:

Task 1: 25
Task 2: 56
Task 3: 42

With inputs 3 and 4:

Task 1: 25
Task 2: 12
Task 3: 42

The key insight here is that your vector stores Task objects uniformly, even though each one internally holds a completely different callable type. The concrete types (function pointer, functor, lambda) are "erased"—the vector doesn't know or care what's inside each Task, only that it can call execute() on any of them.

Cheat sheet

Type erasure allows storing objects of different types uniformly by hiding their concrete types behind a common interface. Unlike templates or inheritance, it lets unrelated types be treated uniformly at runtime.

std::function is a classic example that can hold any callable (functions, lambdas, functors) regardless of type:

#include <functional>
#include <iostream>

void regularFunc(int x) { std::cout << x << "\n"; }

struct Functor {
    void operator()(int x) { std::cout << x * 2 << "\n"; }
};

int main() {
    std::function<void(int)> f;
    
    f = regularFunc;    // Holds a function pointer
    f(5);               // Output: 5
    
    f = Functor{};      // Holds a functor
    f(5);               // Output: 10
    
    f = [](int x) { std::cout << x + 1 << "\n"; };  // Holds a lambda
    f(5);               // Output: 6
}

Type erasure works by wrapping objects in an internal polymorphic hierarchy:

  • A private abstract base class with pure virtual methods
  • A template-derived class that stores the actual object and implements the virtual methods
  • The wrapper stores a pointer to the base class, forwarding calls through virtual functions

This "erases" the original type while preserving behavior, useful for containers of heterogeneous objects or decoupling interfaces from implementations without forcing inheritance.

Try it yourself

#include <iostream>
#include <vector>
#include "Task.h"
#include "Callables.h"

int main() {
    // Read two integers from input
    int a, b;
    std::cin >> a;
    std::cin >> b;
    
    // TODO: Create a std::vector<Task>
    
    // TODO: Add three tasks to the vector:
    // 1. A task wrapping the computeSquare function
    // 2. A task wrapping a Multiplier functor initialized with a and b
    // 3. A task wrapping a lambda that returns 42
    
    // TODO: Iterate through the vector and execute each task
    // Print: "Task [index]: [result]" where index starts at 1
    
    return 0;
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming