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
Copy file name to clipboardExpand all lines: lectures/std_function.md
+27-28Lines changed: 27 additions & 28 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,15 +9,15 @@
9
9
-[Type Erasure (How it works under the hood)](#type-erasure-how-it-works-under-the-hood)
10
10
-[Summary](#summary)
11
11
12
-
In our [previous lecture](lambdas.md) we explored the magic of lambdas. 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.
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.
13
13
14
-
Today we are going to look at the next piece of the puzzle. 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` shines.
14
+
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!
15
15
16
16
## Overview
17
-
My aim for today is to talk about `std::function` - the standard library's polymorphic wrapper for callables. We'll explore why we need it, where it is used in the wild, and, most importantly, we will demystify how it manages to store literally *anything* that can be called. Spoiler alert: it uses a technique called **Type Erasure**, and by the end of this lecture, we'll implement our own simplified version of it.
17
+
My aim for today is to talk about `std::function` - the standard library's wrapper for callables. We'll explore why we need it, where it is used in the wild, and, finally, we will demystify how it manages to store literally *anything* that can be called. Spoiler alert: it uses a technique called **Type Erasure**, and by the end of this lecture, we'll implement our own simplified version of it. So let's dive in!
18
18
19
-
## The problem: Storing Callables
20
-
Imagine we are building a UI framework, and we want to create a `Button` class. The button doesn't have to know *what* should happen when it is clicked, it just knows *when* it is clicked. The user of our `Button` should be able to hook up any logic they want by passing in a callback.
19
+
## The problem: storing callables
20
+
Imagine we are building a UI framework, and we want to create a `Button` class. The button doesn't have to know *what* should happen when it is clicked, it just knows that it *has* been clicked. The user of our `Button` should be able to hook up any logic they want by passing in a callback.
21
21
22
22
Because we want our code to be extremely fast and flexible, we decide to use **templates**. We template the entire `Button` class on the type of the callback:
23
23
@@ -29,8 +29,8 @@ Because we want our code to be extremely fast and flexible, we decide to use **t
@@ -60,6 +60,8 @@ If we just want to create a button and use it in isolation, this works wonderful
60
60
**Problem 1: Heterogeneous Collections**
61
61
A UI framework usually needs a way to draw all buttons on the screen or handle their input in a loop. We typically put them all in a `std::vector`. But `std::vector` requires all its elements to be of the *exact same type*. Because `play_button` is templated on a lambda (a unique, unnamable class generated by the compiler) and `quit_button` is templated on a function pointer, they are two entirely different types. We cannot store them in the same vector!
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_clicks_`. 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 functor inside the same button's list of callbacks.
75
+
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
76
75
-
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 those 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!
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
78
77
79
## Enter `std::function`
78
-
To solve 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.
80
+
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.
79
81
80
82
The syntax for `std::function` is straightforward:
81
83
<!--
@@ -96,8 +98,8 @@ Let's rewrite our `Button` class. We will remove the template from the class and
Notice that `std::function` can be empty! Just like pointers can be `nullptr`, a `std::function` can hold nothing. If we try to call an empty `std::function`, it throws a `std::bad_function_call`[exception](error_handling.md). That's why we check `if (callback)` before calling it, similar to how we check if a pointer is valid.
192
+
Notice that `std::function` can also be empty! Just like pointers can be `nullptr`, a `std::function` can hold nothing. If we try to call an empty `std::function`, it throws a `std::bad_function_call`[exception](error_handling.md). That's why we check `if (callback)` before calling it, similar to how we check if a pointer is valid.
191
193
192
194
## Performance considerations
193
195
Now, before we go rewriting all our code to use `std::function`, we need to have a brief chat about performance. While `std::function` is incredibly flexible, that flexibility comes at a cost.
@@ -212,24 +214,23 @@ Finally, I want to briefly talk about how `std::function` actually works under t
212
214
213
215
This is achieved using a design pattern called **Type Erasure**.
214
216
215
-
Note that you don't have to know this to use `std::function`! It is also very unlikely that you'll ever need to implement your own type-erased wrapper. Still, it is a cool pattern worth knowing about. It's used in several places in the standard library, including `std:shared_ptr`, `std::any`, and, as we'll see, in `std::function` itself.
217
+
> 💡 Note that you don't have to know this to use `std::function`! It is also very unlikely that you'll ever need to implement your own type-erased wrapper. Still, it is a cool pattern worth knowing about. It's used in several places in the standard library, including `std:shared_ptr`, `std::any`, and, as we'll see, in `std::function` itself.
216
218
217
219
To see how it all works, let's build a simplified version of `std::function` that only handles `void()` callables. We'll call it `MyFunction`.
218
220
219
-
The core idea is to use classical inheritance and virtual functions to hide the actual type:
221
+
The core idea is to use classical [inheritance](inheritance.md) and virtual functions to hide the actual type:
220
222
221
223
```cpp
222
224
#include<iostream>
223
225
#include<memory>
224
226
225
227
classMyFunction {
226
228
public:
227
-
// A templated constructor. This is where the magic enters.
228
229
// We accept ANY callable type T.
229
230
template <typenameT>
230
231
MyFunction(T callable) {
231
232
// We instantiate the derived class with the specific type T,
232
-
// but we store it as a pointer to the generic base class!
233
+
// but we store it as a pointer to the generic, non-templated base class!
This is the essence of Type Erasure. The `MyFunction` class itself is *not* templated. The `callable_` pointer just points to some `CallableBase`. The exact type `T` is remembered *only* inside the templated `CallableImpl<T>` class, which is instantiated when the constructor is called. Once constructed, the type `T` is effectively "erased" from the perspective of `MyFunction`. However, when `MyFunction` object is destroyed, it's destructor will call the destructor of the `CallableBase`, which, having a virtual destructor, will call the destructor of `CallableImpl<T>` which will in turn call the destructor of the actual callable object it holds. This is how the object is safely cleaned up without any memory leaks and without `MyFunction` needing to know anything about the concrete type `T`. This is what type erasure is all about!
283
284
284
-
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 just implemented!
285
+
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!
285
286
286
-
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 an interview (like it did for me)!
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)!
287
288
288
289
## Summary
289
-
To sum up, `std::function` is a powerful tool when you 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.
290
-
291
-
But remember, with great power comes great responsibility (and potential heap allocations). Use it for callbacks, event handlers, and storing tasks, but stick to templates when you need maximum performance on hot paths.
290
+
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.
292
291
293
-
And with that, our journey into the world of callables is complete. Feel free to play around with the examples, and try to modify our `MyFunction` to support arguments and return values!
292
+
But remember, with great power comes great responsibility (and potential heap allocations). So use `std::function` for callbacks, event handlers, and storing tasks, but stick to templates when you need maximum performance on hot paths.
294
293
295
-
<!-- Thanks a lot for watching and I'll catch you in the next video, bye! -->
294
+
<!--And that's it for now! Thanks a lot for watching and I'll catch you in the next video, bye! -->
0 commit comments