Skip to content

Commit c6efe85

Browse files
committed
Final text and code animations
1 parent 05112e7 commit c6efe85

2 files changed

Lines changed: 61 additions & 27 deletions

File tree

animation/projects/src/std_function/scenes/std_function.tsx

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,31 +76,48 @@ export default makeScene2D(function* (view) {
7676
yield* codeRef().code(multiButtonCode, 1);
7777
yield* waitFor(1);
7878

79-
yield* centerOn(codeRef(), [lines(7, 8), lines(25, 28), lines(30, 30)], 1, 22);
79+
yield* centerOn(codeRef(), [lines(7, 8)], 1, 30);
80+
yield* waitFor(0.5);
81+
yield* centerOn(codeRef(), [lines(7, 8), lines(19, 19)], 1, 30);
8082
yield* waitFor(3);
8183

84+
yield* centerOn(codeRef(), [lines(25, 28), lines(30, 30)], 1, 30);
85+
yield* waitFor(1);
86+
87+
yield* centerOn(codeRef(), [lines(12, 14)], 1, 30);
88+
yield* waitFor(3);
89+
90+
8291
yield* centerOn(codeRef(), DEFAULT, 1, 22);
8392
yield* waitFor(3);
8493

8594
yield* centerOn(codeRef(), [lines(13, 13)], 1, 35);
8695
yield* waitFor(3);
8796

8897
// 5. Type Erasure
89-
yield* codeRef().code(typeErasureCode, 1);
98+
yield* centerOn(codeRef(), DEFAULT, 0, 20);
99+
yield* codeRef().code(typeErasureCode, 0);
90100
yield* waitFor(1);
91101

92-
yield* centerOn(codeRef(), lines(21, 24), 1, 36); // CallableBase
93-
yield* waitFor(3);
102+
yield* centerOn(codeRef(), lines(3, 3), 1, 32);
103+
yield* waitFor(1);
104+
yield* centerOn(codeRef(), [lines(13, 18), lines(28, 28)], 1, 32);
105+
yield* waitFor(1);
94106

95-
yield* centerOn(codeRef(), lines(29, 36), 1, 36); // CallableImpl
107+
yield* centerOn(codeRef(), lines(19, 27), 1, 36);
108+
yield* waitFor(1);
109+
yield* centerOn(codeRef(), [lines(19, 27), lines(5, 8)], 1, 35);
96110
yield* waitFor(3);
97111

98-
yield* centerOn(codeRef(), lines(6, 11), 1, 36); // MyFunction constructor
112+
yield* centerOn(codeRef(), lines(30, 42), 1, 36);
99113
yield* waitFor(3);
100-
101-
yield* centerOn(codeRef(), lines(41, 41), 1, 36); // std::unique_ptr<CallableBase> callable_;
114+
yield* centerOn(codeRef(), lines(10, 10), 1, 36);
115+
yield* waitFor(1);
116+
yield* centerOn(codeRef(), [lines(16, 16)], 1, 35);
117+
yield* waitFor(1);
118+
yield* centerOn(codeRef(), [lines(23, 23)], 1, 35);
102119
yield* waitFor(3);
103120

104-
yield* centerOn(codeRef(), DEFAULT, 1, 28);
121+
yield* centerOn(codeRef(), DEFAULT, 1, 20);
105122
yield* waitFor(3);
106123
});

lectures/std_function.md

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ int main() {
144144
// ✅ This works now! Both buttons are the EXACT SAME type.
145145
std::vector<Button> buttons{play_button, quit_button};
146146

147-
for (const auto& btn : buttons) { btn.Click(); }
147+
for (const auto& button : buttons) { button.Click(); }
148148
}
149149
```
150150
@@ -161,7 +161,7 @@ Playing game!
161161
Quitting game...
162162
```
163163

164-
And what about our second problem? Now that our callbacks are wrapped in a uniform type, we can easily change our `Button` to store a `std::vector` of different `std::function` objects, allowing us to attach multiple diverse actions to a single button:
164+
And what about our second problem? Now that our callbacks are wrapped in a uniform type, we can easily change our `Button` class to store a `std::vector` of different `std::function` objects, allowing us to attach multiple diverse actions to a single button. We can pass this vector of callbacks in the constructor and store it as a class member:
165165

166166
<!--
167167
`CPP_COPY_SNIPPET` std_function/multi_button.cpp
@@ -202,11 +202,11 @@ int main() {
202202

203203
std::vector<Button> buttons{play_button, quit_button};
204204

205-
for (const auto& btn : buttons) { btn.Click(); }
205+
for (const auto& button : buttons) { button.Click(); }
206206
}
207207
```
208208
209-
The rest of the example stays largely the same with the main difference being that we now pass a vector of callbacks to a button.
209+
We can now pass a vector of callbacks to a button in the `main` function, and call all of them in a `for` loop when a button is clicked. The rest of the example stays largely the same.
210210
211211
When we run the code, all of our callbacks are being called as expected:
212212
@@ -218,7 +218,7 @@ Logging: Play was clicked.
218218
Quitting game...
219219
```
220220

221-
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.
221+
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 can check if a pointer is valid before dereferencing it.
222222

223223
## Performance considerations
224224
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.
@@ -227,18 +227,31 @@ Let's look at the pros and cons of each of the two approaches we just explored.
227227

228228
**Template Approach:**
229229

230-
- **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.
231-
- **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.
232-
- **Con**: As we saw, we cannot easily store these objects in a single standard container, because they all have different underlying types.
230+
- **Pro**: The compiler knows the exact type of the callable at compile time making it easier for the compiler to optimize the code. This approach is fast and never allocates memory on the heap.
231+
- **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. Also, all implementation lives in headers unless we [pre-compile for a selected set of callable types](templates_and_headers.md).
232+
- **Con**: Objects have different types so we can't store them in a single standard container.
233233

234234
**`std::function` Approach:**
235235

236-
- **Pro**: True dynamic, runtime polymorphism! The `std::function` can wrap *any* callable matching the provided signature at runtime.
237-
- **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.
238-
- **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 if used on a hot path.
239-
- **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.
236+
- **Pro**: The `std::function` can wrap *any* callable matching the provided signature **at runtime**.
237+
- **Pro**: All matching callables are wrapped in the exact same type (e.g., `std::function<void()>()`), so we can easily store them in standard containers, pass them around, and swap out their callbacks on the fly.
238+
- **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 call `new` to allocate memory on the heap. This can be a major performance bottleneck if used on a hot path.
239+
- **Con**: **Virtual Call Overhead**. Invoking a `std::function` generally involves an indirect function call (like calling a [virtual function](inheritance.md#runtime-and-memory-overhead-of-using-virtual)), which can make it harder for the compiler to perform all of its optimizations.
240240

241-
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. That being said, when performance really matters - we should always measure the alternatives and pick the option that suits us best!
241+
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.
242+
243+
<!--
244+
`CPP_SKIP_SNIPPET`
245+
-->
246+
```cpp
247+
// std::sort taking a templated callable as a parameter
248+
template<class RandomIt, class Compare>
249+
void sort(RandomIt first, RandomIt last, Compare comp);
250+
```
251+
252+
On the contrary, `std::function` is for situations like UI callbacks and event handlers, where we want to store and maybe even replace different callable types at runtime and the tiny performance overhead of a virtual call doesn't matter.
253+
254+
However, what I just said is just a rule of a thumb. When performance really matters - we should always measure the alternatives and pick the option that suits us best!
242255
243256
## Type Erasure (How it works under the hood)
244257
Finally, I want to briefly talk about how `std::function` actually works under the hood. How can it store an arbitrary callable (say a lambda or a functor) without knowing its exact type at compile time? How does it all get cleaned up neatly without [leaking memory](memory_and_smart_pointers.md)?
@@ -247,9 +260,9 @@ This is achieved using a design pattern called **Type Erasure**.
247260
248261
> 💡 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 pretty 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.
249262
250-
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`.
263+
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` because I have no imagination.
251264
252-
The core idea is to use classical [inheritance](inheritance.md) and virtual functions to hide the actual type:
265+
The core idea is to use a templated class that inherits from a non-templated base class that uses [virtual functions](inheritance.md) to call the stored callable:
253266
254267
<!--
255268
`CPP_SETUP_START`
@@ -288,7 +301,7 @@ class MyFunction {
288301
};
289302
290303
template <typename T>
291-
struct CallableImpl : CallableBase {
304+
struct CallableImpl : public CallableBase {
292305
explicit CallableImpl(T callable) : stored_callable(std::move(callable)) {}
293306
294307
void Invoke() const override { stored_callable(); }
@@ -310,9 +323,13 @@ int main() {
310323
}
311324
```
312325

313-
This is the essence of Type Erasure. The `MyFunction` class itself is *not* templated. The `callable_` pointer just points to some `CallableBase`. By the way, here we inherit from `Noncopyable` base class to make sure our `CallableBase` is only used through a pointer, see the [inheritance lecture](inheritance.md#things-to-know-about-classes-with-virtual-methods) for more details.
326+
Let's unpack what is happening here. The `MyFunction` class itself is *not* templated. The `callable_` pointer just points to some `CallableBase` that has a pure virtual `Invoke()` method. By the way, here we inherit from `Noncopyable` base class to make sure our `CallableBase` only has pointer / reference semantics, not value semantics, see the [inheritance lecture](inheritance.md#things-to-know-about-classes-with-virtual-methods) for more details.
327+
328+
The exact type `T` is remembered *only* inside the templated `CallableImpl<T>` class, which is instantiated when the templated `MyFunction` constructor is called. The `CallableImpl` class overrides the virtual `Invoke()` method and actually calls the stored callable object.
329+
330+
So now if we create a couple of `MyFunction` objects - one with a free function, one with a lambda - the call to `operator()` on either `MyFunction` object will call the `Invoke()` method on the `CallableBase` pointer, which through the vtable finds the actual `Invoke` method on the appropriate `CallableImpl` object, which then calls the stored callable object.
314331

315-
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!
332+
Same logic applies when `MyFunction` object is destroyed, its 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 underlying callable object can be called and 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!
316333

317334
And of course, if we run the code we get the printout that we expect!
318335

0 commit comments

Comments
 (0)