Skip to content

Instantly share code, notes, and snippets.

@MangaD
Last active April 8, 2025 06:53
Show Gist options
  • Save MangaD/7cad1ca49c380edf72cb6cc9ebbfd495 to your computer and use it in GitHub Desktop.
Save MangaD/7cad1ca49c380edf72cb6cc9ebbfd495 to your computer and use it in GitHub Desktop.
C++ Type erasure

Type erasure

CC0

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.”


🧠 Why Use Type Erasure?

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.

✅ You use it when you want:

  • To store and use heterogeneous types uniformly
  • To hide implementation details
  • To decouple interfaces from types
  • To have runtime polymorphism without inheritance

🧰 Real-World Examples of Type Erasure

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

🔍 A Quick Visual Explanation

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.


🧬 How It Works (Simplified Mechanism)

  1. You define a concept interface (what you want all types to do)
  2. Each type is wrapped into a common base interface
  3. You store a pointer to the base, not the concrete type
  4. When you call a method, it dispatches via a virtual function or function pointer

🔧 Minimal Custom Example

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.


🚀 Advantages

  • 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)

⚠️ Downsides

  • Heap allocation and indirection (in many implementations)
  • Harder to debug
  • Less efficient than static polymorphism
  • Interface limited to what you define in the erasure

🧾 TL;DR

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!


What is Type Erasure in C++?

CC0

Disclaimer: Grok generated document.

Definition

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.

Core Idea

  • 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.

How It Works

  • 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.

Example: Basic Type Erasure

#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 of Circle or Square, exposing only draw(). Internally, it uses a Concept base class and templated Impl to bridge compile-time types to runtime behavior.

Key Features

  • 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).

Comparison to Polymorphism

Traditional Polymorphism (Dynamic)

  • 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.

Comparison

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.

Comparison to CRTP

CRTP (Curiously Recurring Template Pattern)

  • 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.

Comparison

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.

Comprehensive Breakdown

Type Erasure: Deep Dive

  • 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).
  • 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.

Polymorphism vs. Type Erasure

  • 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.

CRTP vs. Type Erasure

  • 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.

Practical Example: Mixed Container

  • 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.

Why It Matters for You

  • 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.”

Action Plan

  • Learn:
    • Type Erasure: Build a Logger wrapper—erase FileLogger, ConsoleLogger.
    • Polymorphism: Code a Shape hierarchy—extend it.
    • CRTP: Implement a Matrix class—benchmark vs. virtual.
  • 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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment