Skip to content

Commit 9f9242e

Browse files
committed
Almost final text
1 parent da680ff commit 9f9242e

1 file changed

Lines changed: 27 additions & 28 deletions

File tree

lectures/std_function.md

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@
99
- [Type Erasure (How it works under the hood)](#type-erasure-how-it-works-under-the-hood)
1010
- [Summary](#summary)
1111

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

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!
1515

1616
## 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!
1818

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

2222
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:
2323

@@ -29,8 +29,8 @@ Because we want our code to be extremely fast and flexible, we decide to use **t
2929
template <typename Callback>
3030
class Button {
3131
public:
32-
explicit Button(std::string name, Callback cb)
33-
: name_{std::move(name)}, on_click_{std::move(cb)} {}
32+
explicit Button(std::string name, Callback callback)
33+
: name_{std::move(name)}, on_click_{std::move(callback)} {}
3434

3535
void Click() const {
3636
std::cout << "[" << name_ << "] was clicked.\n";
@@ -60,6 +60,8 @@ If we just want to create a button and use it in isolation, this works wonderful
6060
**Problem 1: Heterogeneous Collections**
6161
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!
6262
63+
<!-- TODO: show this in the cpp insights -->
64+
6365
<!--
6466
`CPP_SKIP_SNIPPET`
6567
-->
@@ -70,12 +72,12 @@ std::vector<Button<???>> buttons{play_button, quit_button};
7072
```
7173

7274
**Problem 2: Multiple Mixed Callbacks**
73-
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.
7476

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!
7678

7779
## 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.
7981

8082
The syntax for `std::function` is straightforward:
8183
<!--
@@ -96,8 +98,8 @@ Let's rewrite our `Button` class. We will remove the template from the class and
9698
// Approach: std::function (Runtime Polymorphism)
9799
class Button {
98100
public:
99-
explicit Button(std::string name, std::function<void()> cb)
100-
: name_{std::move(name)}, on_click_{std::move(cb)} {}
101+
explicit Button(std::string name, std::function<void()> callback)
102+
: name_{std::move(name)}, on_click_{std::move(callback)} {}
101103

102104
void Click() const {
103105
std::cout << "[" << name_ << "] was clicked.\n";
@@ -145,21 +147,21 @@ class MultiButton {
145147
public:
146148
explicit MultiButton(std::string name) : name_{std::move(name)} {}
147149

148-
void AddCallback(std::function<void()> cb) {
149-
on_clicks_.push_back(std::move(cb));
150+
void AddCallback(std::function<void()> callback) {
151+
on_click_callbacks_.push_back(std::move(callback));
150152
}
151153

152154
void Click() const {
153155
std::cout << "[" << name_ << "] was clicked.\n";
154-
for (const auto& callback : on_clicks_) {
156+
for (const auto& callback : on_click_callbacks_) {
155157
if (callback) { callback(); }
156158
}
157159
}
158160

159161
private:
160162
std::string name_;
161163
// ✅ We can store a list of ANY callables!
162-
std::vector<std::function<void()>> on_clicks_;
164+
std::vector<std::function<void()>> on_click_callbacks_;
163165
};
164166

165167
void QuitGame() { std::cout << "Quitting game...\n"; }
@@ -187,7 +189,7 @@ Logging: Play was clicked.
187189
Quitting game...
188190
```
189191

190-
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.
191193

192194
## Performance considerations
193195
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
212214

213215
This is achieved using a design pattern called **Type Erasure**.
214216

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

217219
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`.
218220

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:
220222

221223
```cpp
222224
#include <iostream>
223225
#include <memory>
224226

225227
class MyFunction {
226228
public:
227-
// A templated constructor. This is where the magic enters.
228229
// We accept ANY callable type T.
229230
template <typename T>
230231
MyFunction(T callable) {
231232
// 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!
233234
callable_ = std::make_unique<CallableImpl<T>>(std::move(callable));
234235
}
235236

@@ -281,15 +282,13 @@ Lambda!
281282

282283
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!
283284

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!
285286

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)!
287288

288289
## 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.
292291

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

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

Comments
 (0)