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
EasyLet'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-erasedTaskclass that can store and execute any callable that takes no parameters and returns anint.Your
Taskclass needs an internal polymorphic hierarchy that the user never sees. Create a private abstract base class (likeConcept) with a pure virtualinvoke()method. Then create a template-derived class (likeModel) that stores the actual callable and implementsinvoke()by calling it.The
Taskclass should store astd::unique_ptrto 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
Taskclass hides the concrete type of the callable behind the polymorphic interface.- A template constructor that accepts any callable and wraps it in the appropriate
Callables.h: Define some different callable types to demonstrate that yourTaskcan hold them all.Create a regular function
computeSquare()that returns25(5 squared).Create a functor class
Multiplierthat 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 theMultiplierfunctor.Create a
std::vector<Task>and add three different tasks to it:- A task wrapping the
computeSquarefunction - A task wrapping a
Multiplierfunctor initialized with the two input values - 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.- A task wrapping the
For example, with inputs 7 and 8:
Task 1: 25
Task 2: 56
Task 3: 42With inputs 3 and 4:
Task 1: 25
Task 2: 12
Task 3: 42The 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;
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of OOP
External FilesC++ Build & CompilationHeader Files & Source FilesNamespaces & ScopeIntroduction to OOP in C++Classes vs ObjectsThe 'this' PointerMethods (Member Functions)Attributes (Data Members)Ctors & Dtors BasicsRecap - Simple Calculator4Class Properties
Instance vs Static MembersGetters and SettersConst Member FunctionsMutable KeywordStatic Methods and VariablesFriend Functions & ClassesRecap - Bank Account Manager7Inheritance
Basic InheritanceInheritance Access LevelsCtor & Dtor Call OrderMethod OverridingVirtual Functions & VTableMultiple InheritanceVirtual InheritanceRecap - Employee Hierarchy2Memory Management
Stack vs Heap MemoryPointers and ReferencesDynamic Memory (new/delete)Smart Pointers in C++RAII in C++Recap - Dynamic Array Manager5Encapsulation
Access Specifiers in C++Access Specifiers In DepthInformation HidingStruct vs ClassNested & Inner ClassesRecap - Student Records System8Polymorphism
Compile vs Runtime PolymorphFunction OverloadingVirtual Functions RevisitedPure Virtual FunctionsAbstract ClassesInterface Design in C++Dynamic Casting & RTTIRecap - Shape Calculator11Advanced OOP Concepts
Composition vs InheritanceMixins via CRTPPimpl IdiomType ErasureEnum Classes & Strong TypingException Handling in OOPCustom Exception Hierarchies3Constructors & Destructors
Default ConstructorParameterized ConstructorCopy ConstructorMove ConstructorConstructor Init ListsDelegating ConstructorsDestructor Deep DiveRule of Three / Five / ZeroRecap - String Class6Operator Overloading
Intro to Operator OverloadArithmetic Operator OverloadComparison Operator OverloadStream OperatorsAssignment Operator Overload[] and () Operator OverloadType Conversion OperatorsRecap - Matrix Class9Templates
Function TemplatesClass TemplatesTemplate SpecializationVariadic TemplatesSFINAE & Type Traits BasicsRecap - Generic Container