You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
-[Type Erasure (How it works under the hood)](#type-erasure-how-it-works-under-the-hood)
10
9
-[Summary](#summary)
11
10
12
-
In one of our [previous lectures](lambdas.md) we looked at what lambdas are and how they work. We saw how they give us a clean, inline way to create function objects on the fly, saving us from writing boilerplate structs just to pass a simple comparator or an operation to a standard algorithm.
11
+
In one of our [previous lectures](lambdas.md) we looked at what lambdas are and how they work. We saw how they give us a clean, inline way to create function objects on the fly, saving us from writing boilerplate structs just to pass a simple comparator or an operation to a standard algorithm.
13
12
14
13
But what happens when we want to store these callables for later? What if we want a unified way to handle *anything* that behaves like a function? This is exactly where [`std::function`](https://en.cppreference.com/cpp/utility/functional/function) comes to the rescue!
In addition to that, what if we wanted a single button to trigger *multiple* actions when clicked? We could change `Callback on_click_` to `std::vector<Callback> on_click_callbacks_`. But again, because `std::vector` demands identical types, we could only store multiple copies of the *exact same* lambda or the *exact same* function pointer. We couldn't mix a lambda and a function object inside the same button's list of callbacks.
74
+
In addition to that, what if we wanted a single button to trigger *multiple* actions when clicked? We could change `Callback on_click_` to `std::vector<Callback> on_click_callbacks_`. But again, because `std::vector` demands identical types, we could only store multiple copies of the *exact same* lambda or the *exact same* function pointer. We couldn't mix a lambda and a function object inside the same button's list of callbacks.
76
75
77
-
But what about [`std::variant`](variant.md), I hear some of you ask? Well, technically it would allow us to store various types of callbacks in a single vector, but some ofthose types would be horrible temporary anonymous classes generated by the compiler in the case of a lambda! And so we would have to turn our code inside out to let compiler figure out those types on its own - we wouldn't be able to provide them to our variant upfront!
76
+
But what about [`std::variant`](variant.md), I hear some of you ask? Well, technically it would allow us to store various types of callbacks in a single vector, but some ofthose types would be horrible temporary anonymous classes generated by the compiler in the case of a lambda! And so we would have to turn our code inside out to let compiler figure out those types on its own - we wouldn't be able to provide them to our variant upfront!
78
77
79
78
## Enter `std::function`
80
79
To solve all of these problems we can use `std::function`, introduced in C++11 and available in the `<functional>` header. It is a wrapper that can store, copy, and invoke *any* callable target — be it a function, a lambda expression, a bind expression, or a function object — as long as its signature matches the one specified in the `std::function` template argument.
@@ -197,15 +196,17 @@ Now, before we go rewriting all our code to use `std::function`, we need to have
197
196
Let's look at the pros and cons of each of the two approaches we just explored.
198
197
199
198
**Template Approach:**
200
-
***Pro**: Zero overhead. The compiler knows the exact type of the callable at compile time, leading to aggressive inlining. It is blazing fast and never allocates memory on the heap.
201
-
***Con**: It relies on static, compile-time polymorphism. For every different callable type we pass, the compiler generates a completely new class type or function. Oh, also all implementation lives in headers.
202
-
***Con**: As we saw, we cannot easily store these objects in a single standard container, because they all have different underlying types.
199
+
200
+
-**Pro**: Zero overhead. The compiler knows the exact type of the callable at compile time, leading to aggressive inlining. It is blazing fast and never allocates memory on the heap.
201
+
-**Con**: It relies on static, compile-time polymorphism. For every different callable type we pass, the compiler generates a completely new class type or function. Oh, also all implementation lives in headers.
202
+
-**Con**: As we saw, we cannot easily store these objects in a single standard container, because they all have different underlying types.
203
203
204
204
**`std::function` Approach:**
205
-
***Pro**: True dynamic, runtime polymorphism! The `Button` is a single, concrete class. It can accept *any* callable matching the signature at runtime.
206
-
***Pro**: Because all matching callables are wrapped in the exact same type (e.g., `std::function<void()>`), we can easily store them in vectors, pass them around, and swap out their callbacks on the fly.
207
-
***Con**: **Heap Allocation**. Implementations of `std::function` usually have a small internal buffer (Small Object Optimization) to store small callables, which is quite fast to use. But if our callable's state (like a lambda with a large capture list) is too large for the internal buffer, `std::function` will silently call `new` to allocate memory on the heap. This can be a major performance hit.
208
-
***Con**: **Virtual Call Overhead**. Invoking a `std::function` generally involves an indirect function call (like calling a virtual function), which can defeat compiler inlining and branch prediction. That being said, always measure when performance is important!
205
+
206
+
-**Pro**: True dynamic, runtime polymorphism! The `Button` is a single, concrete class. It can accept *any* callable matching the signature at runtime.
207
+
-**Pro**: Because all matching callables are wrapped in the exact same type (e.g., `std::function<void()>`), we can easily store them in vectors, pass them around, and swap out their callbacks on the fly.
208
+
-**Con**: **Heap Allocation**. Implementations of `std::function` usually have a small internal buffer (Small Object Optimization) to store small callables, which is quite fast to use. But if our callable's state (like a lambda with a large capture list) is too large for the internal buffer, `std::function` will silently call `new` to allocate memory on the heap. This can be a major performance hit.
209
+
-**Con**: **Virtual Call Overhead**. Invoking a `std::function` generally involves an indirect function call (like calling a virtual function), which can defeat compiler inlining and branch prediction. That being said, always measure when performance is important!
209
210
210
211
Because of this, if we are writing performance-critical code on hot paths (like a `std::sort` algorithm that runs a callable we pass into it millions of times a second), we typically stick to templates. `std::function` is for situations like UI callbacks and event handlers, where we *must* store different types at runtime and the tiny performance overhead of a virtual call doesn't matter.
211
212
@@ -257,7 +258,7 @@ class MyFunction {
257
258
T stored_callable;
258
259
};
259
260
260
-
// 3. We only store a pointer to the base class.
261
+
// 3. We only store a pointer to the base class.
261
262
// The exact type T is erased!
262
263
std::unique_ptr<CallableBase> callable_;
263
264
};
@@ -284,7 +285,7 @@ This is the essence of Type Erasure. The `MyFunction` class itself is *not* temp
284
285
285
286
Real `std::function` is of course much more complex. It handles different return types and arguments, uses Small Object Optimization to avoid `std::unique_ptr` heap allocations when possible, and provides copy semantics. I won't go into these details here. But the underlying principle is exactly what we've just implemented!
286
287
287
-
> ⚠️ Finally, it won't hurt to stress this again: you'll probably never need to implement your own type-erased wrapper. But it's good to know how it works under the hood. Who knows, maybe it'll come up in a job interview (like it did for me, looking at you, Misha)!
288
+
> ⚠️ Finally, it won't hurt to stress this again: you'll probably never need to implement your own type-erased wrapper. But it's good to know how it works under the hood. Who knows, maybe it'll come up in a job interview (like it did for me, looking at you, Misha)!
288
289
289
290
## Summary
290
291
To sum up, `std::function` is a powerful tool when we need a generic, uniform way to store and pass around things that can be called. It bridges the gap between the static compile-time world of lambdas/functors and the dynamic runtime world of vectors and callbacks.
0 commit comments