C++ - Type erasure

2 minute read

Duck Typing, the C++ Way: How Type Erasure Bends the Rules - Sarthak Sehgal - CppCon 2025

std::function (Variadic Template Specialization)

To make the type-erased function wrapper work for any signature, we forward-declare a primary template and provide a partial specialization that splits the signature into a return type (R) and a variadic parameter pack (Args...).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <memory>
#include <utility>
#include <stdexcept>

// 1. Primary template (forward declaration)
template <typename Signature>
class function;

// 2. Partial specialization for a function signature returning R and taking Args...
template <typename R, typename... Args>
class function<R(Args...)> {

    // Concept: Abstract base class using the variadic arguments
    struct Concept {
        virtual ~Concept() = default;
        virtual R operator()(Args... args) const = 0;
        virtual std::unique_ptr<Concept> clone() const = 0;
    };

    // Model: Concrete implementation that holds the callable
    template <typename F>
    struct Model : Concept {
        F callable;
        
        Model(F f) : callable(std::move(f)) {}
        
        // Overrides the concept's operator() and perfectly forwards the arguments
        R operator()(Args... args) const override { 
            return callable(std::forward<Args>(args)...); 
        }
        
        std::unique_ptr<Concept> clone() const override { 
            return std::make_unique<Model>(*this); 
        }
    };

    std::unique_ptr<Concept> pimpl;

public:
    function() = default;

    // Templated constructor captures the concrete callable 'F'
    // (In C++20, you would typically add `requires std::invocable<F, Args...>` here)
    template <typename F>
    function(F f) : pimpl(std::make_unique<Model<F>>(std::move(f))) {}
    
    // Copy constructor using the clone pattern
    function(const function& other) {
        if (other.pimpl) {
            pimpl = other.pimpl->clone();
        }
    }
    
    // The user-facing function call operator
    R operator()(Args... args) const {
        if (!pimpl) {
            throw std::bad_function_call(); // Throw if the function is empty
        }
        return (*pimpl)(std::forward<Args>(args)...);
    }
};

std:;shared_ptr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Non-templated base class for the control block
struct ControlBlockBase {
    size_t ref_count;
    virtual ~ControlBlockBase() = default;
};

// Templated derived class storing the precise type and deleter
template <typename Y, typename Deleter>
struct ControlBlock : ControlBlockBase {
    Y* ptr;
    Deleter d;
    
    ControlBlock(Y* p, Deleter deleter) : ptr(p), d(deleter) {}
    
    ~ControlBlock() override {
        d(ptr); // Invokes the deleter with the correct type
    }
};

// The shared_ptr wrapper
template <typename T>
class shared_ptr {
    T* ptr;
    ControlBlockBase* control_block;

public:
    template <typename Y, typename Deleter>
    shared_ptr(Y* p, Deleter d) {
        ptr = p;
        control_block = new ControlBlock<Y, Deleter>(p, d);
    }
    
    ~shared_ptr() {
        // When ref_count hits 0:
        delete control_block; // Virtual destructor cleans up the correct ControlBlock
    }
};

std::any

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class any {
    void* data = nullptr;
    void (*deleter)(void*) = nullptr;

public:
    // Templated constructor knows the concrete type 'T'
    template <typename T>
    any(T value) {
        data = new T(std::move(value));
        
        // Capture the exact destruction logic
        deleter = [](void* ptr) {
            delete static_cast<T*>(ptr);
        };
    }

    ~any() {
        if (deleter && data) {
            deleter(data);
        }
    }
};

Categories:

Updated: