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);
}
}
};
|