Disclaimer: ChatGPT generated document.
Type erasure in C++ is a powerful technique that allows you to hide the concrete type of an object behind a uniform interface — so you can work with objects of different types without knowing their actual types at compile time.
It’s like saying: “I don’t care what type you are, as long as you behave in a certain way.”
Because C++ is statically typed, you normally need to know the exact type of an object at compile time. But sometimes, you want the flexibility of dynamic polymorphism, without inheritance or templates — and type erasure gives you that.
- To store and use heterogeneous types uniformly
- To hide implementation details
- To decouple interfaces from types
- To have runtime polymorphism without inheritance
Use Case | Example |
---|---|
Any value of any type | std::any |
Callable object of any type | std::function |
Iterators of any type | std::ranges::any_view (C++20) |
Event systems / plugin frameworks | Custom interfaces via type erasure |
Imagine you want a variable that holds anything callable:
std::function<void()> f;
f = []() { std::cout << "Hello\n"; }; // a lambda
f(); // You don't need to know the lambda's actual type!
Behind the scenes, std::function
erases the actual type of the lambda or function pointer, wraps it in a type-erased interface, and stores it via a base class pointer or virtual table.
- You define a concept interface (what you want all types to do)
- Each type is wrapped into a common base interface
- You store a pointer to the base, not the concrete type
- When you call a method, it dispatches via a virtual function or function pointer
Say we want to store any object that has a print()
method:
struct Printable {
virtual void print() const = 0;
virtual ~Printable() = default;
};
template<typename T>
class PrintableModel : public Printable {
T obj;
public:
PrintableModel(T x) : obj(std::move(x)) {}
void print() const override { obj.print(); }
};
class AnyPrintable {
std::unique_ptr<Printable> ptr;
public:
template<typename T>
AnyPrintable(T x) : ptr(std::make_unique<PrintableModel<T>>(std::move(x))) {}
void print() const { ptr->print(); }
};
Now you can do:
struct Hello {
void print() const { std::cout << "Hello\n"; }
};
struct World {
void print() const { std::cout << "World\n"; }
};
AnyPrintable a = Hello{};
AnyPrintable b = World{};
a.print(); // Hello
b.print(); // World
➡️ You erased the types Hello
and World
, but still use them polymorphically.
- No need for a shared base class or inheritance
- Great for building generic containers, plugin systems, or dependency injection
- Can combine multiple behaviors (e.g., using concepts or lambdas)
- Heap allocation and indirection (in many implementations)
- Harder to debug
- Less efficient than static polymorphism
- Interface limited to what you define in the erasure
Concept | Explanation |
---|---|
Type erasure | Technique to abstract away the concrete type behind a uniform interface |
Common use | std::function , std::any , plugin APIs, runtime polymorphism |
Benefit | Allows handling different types with a common interface |
Cost | Slight runtime overhead, possible heap allocations |
Would you like:
- A hands-on project using type erasure?
- A comparison of type erasure vs. inheritance vs. templates?
- Or help implementing your own type-erased system?
Let’s build something cool with it!
Disclaimer: Grok generated document.
Type erasure is a technique in C++ that hides the concrete type of an object behind a uniform interface, allowing heterogeneous types to be treated uniformly at runtime without requiring a common base class or virtual functions. It "erases" compile-time type information, replacing it with a runtime abstraction, often to achieve flexibility or decouple code.
- You define an interface (e.g., a concept like "drawable") that multiple types can implement.
- A single wrapper class holds these types, abstracting their differences—users interact only with the wrapper.
- Unlike traditional inheritance, it avoids virtual function overhead or forces type relationships.
- Typically implemented using:
- Templates: Capture type-specific behavior at compile time.
- Dynamic Polymorphism: Use a base class pointer with virtual functions internally.
- Heap Allocation: Store concrete objects via
std::unique_ptr
or similar.
- The wrapper exposes a generic interface (e.g.,
draw()
), delegating to the hidden type’s implementation.
#include <memory>
#include <iostream>
class Drawable {
public:
// Public interface
void draw() const {
impl_->draw(); // Delegate to concrete type
}
// Template constructor captures any drawable type
template<typename T>
Drawable(T obj) : impl_(std::make_unique<Impl<T>>(std::move(obj))) {}
private:
// Abstract base for internal polymorphism
struct Concept {
virtual ~Concept() = default;
virtual void draw() const = 0;
};
// Concrete implementation for each type T
template<typename T>
struct Impl : Concept {
Impl(T obj) : obj_(std::move(obj)) {}
void draw() const override { obj_.draw(); }
T obj_;
};
std::unique_ptr<Concept> impl_; // Holds any drawable type
};
// Types to erase
struct Circle {
void draw() const { std::cout << "Drawing Circle\n"; }
};
struct Square {
void draw() const { std::cout << "Drawing Square\n"; }
};
int main() {
Drawable circle(Circle{});
Drawable square(Square{});
circle.draw(); // "Drawing Circle"
square.draw(); // "Drawing Square"
}
- Explanation:
Drawable
erases the type ofCircle
orSquare
, exposing onlydraw()
. Internally, it uses aConcept
base class and templatedImpl
to bridge compile-time types to runtime behavior.
- Flexibility: Works with unrelated types—no inheritance required.
- Cost: Heap allocation + virtual call overhead (similar to polymorphism).
- Use Case: When you need a uniform interface for disparate types (e.g., a container of "loggers" with different backends).
- Definition: Uses inheritance and virtual functions to achieve runtime flexibility—base class defines an interface, derived classes implement it.
- Example:
#include <memory>
#include <iostream>
struct Shape {
virtual ~Shape() = default;
virtual void draw() const = 0;
};
struct Circle : Shape {
void draw() const override { std::cout << "Drawing Circle\n"; }
};
struct Square : Shape {
void draw() const override { std::cout << "Drawing Square\n"; }
};
int main() {
std::unique_ptr<Shape> circle = std::make_unique<Circle>();
std::unique_ptr<Shape> square = std::make_unique<Square>();
circle->draw(); // "Drawing Circle"
square->draw(); // "Drawing Square"
}
- Mechanism: Virtual function table (vtable) resolves calls at runtime.
Aspect | Type Erasure | Polymorphism |
---|---|---|
Type Relationship | No inheritance needed—any type works | Requires common base class |
Compile-Time | Templates capture type info | Fixed hierarchy at compile time |
Runtime Cost | Virtual calls + heap allocation | Virtual calls + heap allocation |
Flexibility | High—unrelated types conform via concept | Lower—inheritance enforces structure |
Code Complexity | Higher—manual wrapper implementation | Simpler—use virtual keyword |
Use Case | Heterogeneous collections (e.g., tools) | Related objects (e.g., shapes) |
- When to Use:
- Polymorphism: Natural fit for hierarchical designs (e.g., GUI widgets)—simpler if types share a base.
- Type Erasure: Better for unrelated types (e.g., third-party loggers)—avoids forcing inheritance.
- Definition: A compile-time technique where a base class template is parameterized by its derived class, enabling static polymorphism (no runtime overhead).
- Example:
#include <iostream>
template<typename Derived>
struct Shape {
void draw() const {
static_cast<const Derived*>(this)->draw_impl(); // Static dispatch
}
};
struct Circle : Shape<Circle> {
void draw_impl() const { std::cout << "Drawing Circle\n"; }
};
struct Square : Shape<Square> {
void draw_impl() const { std::cout << "Drawing Square\n"; }
};
int main() {
Circle circle;
Square square;
circle.draw(); // "Drawing Circle"
square.draw(); // "Drawing Square"
}
- Mechanism: Calls resolved at compile time—no vtable, no heap.
Aspect | Type Erasure | CRTP |
---|---|---|
Type Relationship | No inheritance needed—concept-based | Requires inheritance from template |
Compile-Time | Partial—templates + runtime dispatch | Full—static dispatch only |
Runtime Cost | Virtual calls + heap allocation | Zero—inline calls |
Flexibility | High—unrelated types via wrapper | Lower—derived classes only |
Code Complexity | Higher—wrapper + polymorphism | Moderate—template boilerplate |
Use Case | Runtime collections (e.g., tools) | Performance-critical (e.g., math) |
- When to Use:
- CRTP: Ideal for performance (e.g., matrix libraries in Basel’s pharma sims)—no runtime cost, but types must inherit.
- Type Erasure: Suits runtime flexibility (e.g., Zurich’s trading systems)—handles diverse types dynamically.
- Implementation Variants:
- Small Buffer Optimization (SBO): Avoids heap for small types—stores data in a fixed buffer (e.g.,
std::variant
-like). - External Polymorphism: Separates interface from type (e.g., Boost.TypeErasure).
- Small Buffer Optimization (SBO): Avoids heap for small types—stores data in a fixed buffer (e.g.,
- Pros:
- Decouples types—perfect for APIs or plugins (e.g., a Swiss bank’s extensible tools).
- No need to modify existing classes.
- Cons:
- Overhead (vtable, heap)—not zero-cost like CRTP.
- Boilerplate—requires careful design (e.g.,
Concept
/Impl
).
- Real-World:
std::function
uses type erasure—wraps any callable (e.g., lambdas) into a uniform type.
- Similarity: Both use runtime dispatch—type erasure builds on polymorphism internally.
- Difference: Polymorphism enforces a hierarchy; type erasure enforces a concept (e.g., “has
draw()
”). - Swiss Context: Polymorphism fits Novartis’s device hierarchies; type erasure suits UBS’s third-party integrations.
- Similarity: Both leverage templates—CRTP for static, type erasure for dynamic flexibility.
- Difference: CRTP is compile-time only—can’t store mixed types in a container; type erasure shines here.
- Swiss Context: CRTP powers ZHAW’s real-time robotics; type erasure handles Zug’s crypto platform diversity.
- Type Erasure:
std::vector<Drawable> shapes = {Circle{}, Square{}};
for (const auto& shape : shapes) shape.draw();
- Polymorphism:
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>());
shapes.push_back(std::make_unique<Square>());
for (const auto& shape : shapes) shape->draw();
- CRTP: Not possible—static types don’t mix in a single container.
- Switzerland’s Tech Scene:
- Zurich (Fintech): Type erasure for flexible trading tools—e.g., wrapping diverse data sources.
- Basel (Pharma): Polymorphism for device hierarchies; CRTP for sim speed.
- Zug (Crypto): Type erasure for blockchain plugin systems.
- Career: Mastering these shows depth—e.g., optimizing C++ at Google Zurich (CRTP) or integrating APIs at Scalable Solutions (type erasure).
- Interviews: Explain trade-offs—e.g., “Type erasure trades performance for flexibility vs. CRTP’s zero-cost static dispatch.”
- Learn:
- Type Erasure: Build a
Logger
wrapper—eraseFileLogger
,ConsoleLogger
. - Polymorphism: Code a
Shape
hierarchy—extend it. - CRTP: Implement a
Matrix
class—benchmark vs. virtual.
- Type Erasure: Build a
- Apply: Use in a GitHub project—e.g., a Swiss-inspired “bank transaction sorter.”
Questions? Want a deeper dive (e.g., SBO in type erasure)? Let me know!