Skip to content

Commit 8172786

Browse files
committed
Final text
1 parent 593828a commit 8172786

1 file changed

Lines changed: 17 additions & 16 deletions

File tree

lectures/std_function.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
# `std::function`
1+
`std::function`
22
--
3-
43
- [`std::function`](#stdfunction)
54
- [Overview](#overview)
6-
- [The problem: Storing Callables](#the-problem-storing-callables)
5+
- [The problem: storing callables](#the-problem-storing-callables)
76
- [Enter `std::function`](#enter-stdfunction)
87
- [Performance considerations](#performance-considerations)
98
- [Type Erasure (How it works under the hood)](#type-erasure-how-it-works-under-the-hood)
109
- [Summary](#summary)
1110

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

1413
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!
1514

@@ -72,9 +71,9 @@ std::vector<Button<???>> buttons{play_button, quit_button};
7271
```
7372

7473
**Problem 2: Multiple Mixed 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+
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.
7675

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

7978
## Enter `std::function`
8079
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.
@@ -161,7 +160,7 @@ class MultiButton {
161160
private:
162161
std::string name_;
163162
// ✅ We can store a list of ANY callables!
164-
std::vector<std::function<void()>> on_click_callbacks_;
163+
std::vector<std::function<void()>> on_click_callbacks_;
165164
};
166165

167166
void QuitGame() { std::cout << "Quitting game...\n"; }
@@ -197,15 +196,17 @@ Now, before we go rewriting all our code to use `std::function`, we need to have
197196
Let's look at the pros and cons of each of the two approaches we just explored.
198197

199198
**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.
203203

204204
**`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!
209210

210211
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.
211212

@@ -257,7 +258,7 @@ class MyFunction {
257258
T stored_callable;
258259
};
259260

260-
// 3. We only store a pointer to the base class.
261+
// 3. We only store a pointer to the base class.
261262
// The exact type T is erased!
262263
std::unique_ptr<CallableBase> callable_;
263264
};
@@ -284,7 +285,7 @@ This is the essence of Type Erasure. The `MyFunction` class itself is *not* temp
284285

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

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

289290
## Summary
290291
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

Comments
 (0)