From 9337f6eac2175d58ca897f10ea3656599f2e3a15 Mon Sep 17 00:00:00 2001 From: Igor Bogoslavskyi Date: Mon, 4 May 2026 09:25:49 +0200 Subject: [PATCH 01/10] Initial text for std function --- lectures/std_function.md | 291 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 lectures/std_function.md diff --git a/lectures/std_function.md b/lectures/std_function.md new file mode 100644 index 0000000..342ebd1 --- /dev/null +++ b/lectures/std_function.md @@ -0,0 +1,291 @@ +# `std::function` +-- + +- [`std::function`](#stdfunction) +- [Overview](#overview) +- [The problem: Storing Callables](#the-problem-storing-callables) +- [Enter `std::function`](#enter-stdfunction) +- [Real World Use Cases](#real-world-use-cases) +- [Performance considerations](#performance-considerations) +- [Type Erasure (How it works under the hood)](#type-erasure-how-it-works-under-the-hood) +- [Summary](#summary) + +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. + +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. + +## Overview +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. + +## The problem: Storing Callables +To understand the solution, we must first feel the pain of the problem. + +Let's say we are writing a game and we want to have a list of tasks or events that need to be executed at the end of the frame. Some of these might be simple functions, some might be lambdas, and some might be stateful function objects (functors). + +Let's try to put them all into a `std::vector`: + + +```cpp +#include +#include + +void PrintHello() { std::cout << "Hello!\n"; } + +struct Printer { + void operator()() const { std::cout << "Printing from functor!\n"; } +}; + +int main() { + const auto lambda_printer = [] { std::cout << "Printing from lambda!\n"; }; + + // ❌ Won't compile. What is the type of the vector? + // std::vector tasks; + // tasks.push_back(PrintHello); + // tasks.push_back(Printer{}); + // tasks.push_back(lambda_printer); +} +``` + +We have a bit of an issue here. What is the type of the vector? +- `PrintHello` decays to a function pointer `void(*)()`. +- `Printer` is a class type `Printer`. +- `lambda_printer` is... well, an unnamable, unique class generated by the compiler. + +Even though all three of them take no arguments and return `void`, they are **three completely different types**. We cannot simply throw them into the same `std::vector` because standard containers require all elements to have the exact same type. + +## Enter `std::function` +To solve this, C++11 introduced [`std::function`](https://en.cppreference.com/w/cpp/utility/functional/function), available in the `` header. + +`std::function` is a generic, polymorphic 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. + +Here is how we fix our game loop example: + +```cpp +#include +#include +#include + +void PrintHello() { std::cout << "Hello!\n"; } + +struct Printer { + void operator()() const { std::cout << "Printing from functor!\n"; } +}; + +int main() { + const auto lambda_printer = [] { std::cout << "Printing from lambda!\n"; }; + + // ✅ We use std::function meaning "takes nothing, returns void" + std::vector> tasks; + + tasks.push_back(PrintHello); + tasks.push_back(Printer{}); + tasks.push_back(lambda_printer); + + for (const auto& task : tasks) { + task(); // Execute the callable + } +} +``` +When we run this, it works perfectly! +``` +Hello! +Printing from functor! +Printing from lambda! +``` + +The syntax for `std::function` is straightforward: + +```cpp +std::function +``` +So a callable that takes an `int` and a `float` and returns a `double` would be wrapped in a `std::function`. + +## Real World Use Cases +Where do we actually use this? A very common scenario is in UI programming or event-driven systems where you need **callbacks**. + +Imagine we have a `Button` class. The button doesn't know *what* should happen when it is clicked, it just knows *when* it is clicked. The user of the `Button` should be able to hook up any logic they want. + +```cpp +#include +#include +#include + +class Button { + public: + explicit Button(std::string name) : name_{std::move(name)} {} + + // We allow the user to register any callable that takes no arguments and returns void. + void SetOnClickCallback(std::function callback) { + on_click_ = std::move(callback); + } + + void Click() const { + std::cout << "[" << name_ << "] was clicked.\n"; + if (on_click_) { + on_click_(); // Invoke the callback if it exists + } + } + + private: + std::string name_; + std::function on_click_; +}; + +class GameEngine { + public: + void Quit() { std::cout << "Shutting down the engine...\n"; } +}; + +int main() { + Button quit_button{"Quit"}; + GameEngine engine; + + // We capture the engine by reference in a lambda and pass it to the button + quit_button.SetOnClickCallback([&engine] { + engine.Quit(); + }); + + // Somewhere else in the code, the user clicks the button + quit_button.Click(); +} +``` + +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. That's why we check `if (on_click_)` before calling it, similar to how we check if a pointer is valid. + +## Performance considerations +Now, before we go rewriting all our template code to use `std::function`, we need to have a serious talk about performance. `std::function` is incredibly flexible, but that flexibility comes at a cost. + +When you assign a callable to a `std::function`, where does it store the state? For example, if a lambda captures 10 integers by value, that state needs to live somewhere. +1. **Small Object Optimization (SOO)**: Implementations of `std::function` usually have a small internal buffer (often the size of a few pointers). If your callable fits within this buffer, no heap allocation is made. +2. **Heap Allocation**: If your callable's state 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 you are creating and destroying these objects in a tight loop. +3. **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. + +Because of this, if we are writing performance-critical code (like a `std::sort` algorithm), we still prefer to use **templates** to pass callables. + +Let's do a direct comparison of passing a callable via templates vs via `std::function`: + +```cpp +#include +#include + +// Approach 1: Templates (Compile-time Polymorphism) +template +void RunWithTemplate(Callable callback) { + callback(); +} + +// Approach 2: std::function (Runtime Polymorphism) +void RunWithStdFunction(const std::function& callback) { + callback(); +} + +int main() { + const auto lambda = [] { std::cout << "Working hard!\n"; }; + + RunWithTemplate(lambda); + RunWithStdFunction(lambda); +} +``` + +If we run this code, both functions will do exactly the same thing, but the implications of using one over the other are vastly different. Let's break down the pros and cons! + +**The Template Approach:** +* **Pro**: Zero overhead. The compiler knows the exact type of the callable at compile time, leading to aggressive inlining. It is blazing fast. +* **Con**: It relies on static, compile-time polymorphism. For every different callable type we pass, the compiler generates a whole new version of `RunWithTemplate`. +* **Con**: We cannot easily store different callables in a single standard container, because they all have different underlying types. + +**The `std::function` Approach:** +* **Pro**: True dynamic, runtime polymorphism! A single `RunWithStdFunction` is compiled into the binary, and it can accept *any* callable matching the signature at runtime. +* **Pro**: Because all matching callables are wrapped in the exact same type (e.g., `std::function`), we can easily store them in vectors, swap them out on the fly at runtime, or reassign them (like in our `Button` callback example). +* **Con**: It can incur heap allocations and virtual function call overhead, making it less suitable for extremely hot paths. + +## Type Erasure (How it works under the hood) +Alright, time for the magic trick. How does `std::function` do it? How can it store an `int`, a lambda, or a large struct without knowing their exact types at compile time? + +The technique is called **Type Erasure**. It's a design pattern that allows us to hide the exact type of an object behind a generic interface. + +Let's build a simplified version of `std::function` that only handles `void()` callables. We'll call it `MyFunction`. + +The core idea is to use classical inheritance and virtual functions to hide the actual type: + +```cpp +#include +#include + +class MyFunction { + public: + // A templated constructor. This is where the magic enters. + // We accept ANY callable type T. + template + MyFunction(T callable) { + // We instantiate the derived class with the specific type T, + // but we store it as a pointer to the generic base class! + callable_ = std::make_unique>(std::move(callable)); + } + + // To call it, we just invoke the virtual method on the base class. + void operator()() const { + if (callable_) { + callable_->Invoke(); + } + } + + private: + // 1. The generic interface (Base Class) + struct CallableBase { + virtual ~CallableBase() = default; + virtual void Invoke() const = 0; + }; + + // 2. The concrete implementation (Derived Class) + // This class knows exactly what type T is. + template + struct CallableImpl : CallableBase { + explicit CallableImpl(T callable) : stored_callable(std::move(callable)) {} + + void Invoke() const override { + stored_callable(); // Call the actual underlying object + } + + T stored_callable; + }; + + // 3. We only store a pointer to the base class. + // The exact type T is erased! + std::unique_ptr callable_; +}; + +void FreeFunction() { std::cout << "Free function!\n"; } + +int main() { + MyFunction func1(FreeFunction); + MyFunction func2([] { std::cout << "Lambda!\n"; }); + + func1(); + func2(); +} +``` +If we run this: +``` +Free function! +Lambda! +``` + +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` class, which is instantiated when the constructor is called. Once constructed, the type `T` is effectively "erased" from the perspective of `MyFunction`. + + + +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. But the underlying principle is exactly what we just implemented! + +## Summary +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. + +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. + +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! + + From 8effb5889a6ef25088b7503670f9aa9938f02e72 Mon Sep 17 00:00:00 2001 From: Igor Bogoslavskyi Date: Mon, 4 May 2026 17:36:47 +0200 Subject: [PATCH 02/10] Almost finalized text and examples --- lectures/std_function.md | 264 ++++++++++++++++++++------------------- 1 file changed, 134 insertions(+), 130 deletions(-) diff --git a/lectures/std_function.md b/lectures/std_function.md index 342ebd1..034dc3c 100644 --- a/lectures/std_function.md +++ b/lectures/std_function.md @@ -5,7 +5,6 @@ - [Overview](#overview) - [The problem: Storing Callables](#the-problem-storing-callables) - [Enter `std::function`](#enter-stdfunction) -- [Real World Use Cases](#real-world-use-cases) - [Performance considerations](#performance-considerations) - [Type Erasure (How it works under the hood)](#type-erasure-how-it-works-under-the-hood) - [Summary](#summary) @@ -18,82 +17,65 @@ Today we are going to look at the next piece of the puzzle. What happens when we 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. ## The problem: Storing Callables -To understand the solution, we must first feel the pain of the problem. +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. -Let's say we are writing a game and we want to have a list of tasks or events that need to be executed at the end of the frame. Some of these might be simple functions, some might be lambdas, and some might be stateful function objects (functors). +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: -Let's try to put them all into a `std::vector`: - - ```cpp #include -#include +#include -void PrintHello() { std::cout << "Hello!\n"; } +// Approach: Templates (Compile-time Polymorphism) +template +class Button { + public: + explicit Button(std::string name, Callback cb) + : name_{std::move(name)}, on_click_{std::move(cb)} {} -struct Printer { - void operator()() const { std::cout << "Printing from functor!\n"; } + void Click() const { + std::cout << "[" << name_ << "] was clicked.\n"; + on_click_(); + } + + private: + std::string name_; + Callback on_click_; }; +void QuitGame() { std::cout << "Quitting game...\n"; } + int main() { - const auto lambda_printer = [] { std::cout << "Printing from lambda!\n"; }; + auto lambda_play = [] { std::cout << "Playing game!\n"; }; + + Button play_button{"Play", lambda_play}; + Button quit_button{"Quit", QuitGame}; - // ❌ Won't compile. What is the type of the vector? - // std::vector tasks; - // tasks.push_back(PrintHello); - // tasks.push_back(Printer{}); - // tasks.push_back(lambda_printer); + play_button.Click(); + quit_button.Click(); } ``` -We have a bit of an issue here. What is the type of the vector? -- `PrintHello` decays to a function pointer `void(*)()`. -- `Printer` is a class type `Printer`. -- `lambda_printer` is... well, an unnamable, unique class generated by the compiler. - -Even though all three of them take no arguments and return `void`, they are **three completely different types**. We cannot simply throw them into the same `std::vector` because standard containers require all elements to have the exact same type. - -## Enter `std::function` -To solve this, C++11 introduced [`std::function`](https://en.cppreference.com/w/cpp/utility/functional/function), available in the `` header. - -`std::function` is a generic, polymorphic 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. +If we just want to create a button and use it in isolation, this works wonderfully! It's blazing fast because the compiler knows the exact type of the callback at compile time and can optimize it perfectly. However, as soon as we try to build a more complex and dynamic system, things get more complicated. -Here is how we fix our game loop example: +**Problem 1: Heterogeneous Collections** +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! + ```cpp -#include -#include -#include - -void PrintHello() { std::cout << "Hello!\n"; } - -struct Printer { - void operator()() const { std::cout << "Printing from functor!\n"; } -}; +// ❌ Won't compile. What is the type of the vector? +// play_button and quit_button are completely DIFFERENT class types! +std::vector> buttons{play_button, quit_button}; +``` -int main() { - const auto lambda_printer = [] { std::cout << "Printing from lambda!\n"; }; +**Problem 2: Multiple Mixed Callbacks** +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 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. - // ✅ We use std::function meaning "takes nothing, returns void" - std::vector> tasks; - - tasks.push_back(PrintHello); - tasks.push_back(Printer{}); - tasks.push_back(lambda_printer); +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! - for (const auto& task : tasks) { - task(); // Execute the callable - } -} -``` -When we run this, it works perfectly! -``` -Hello! -Printing from functor! -Printing from lambda! -``` +## Enter `std::function` +To solve these problems we can use `std::function`, introduced in C++11 and available in the `` 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. The syntax for `std::function` is straightforward: +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! -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. But the underlying principle is exactly what we just implemented! +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)! ## Summary 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. From 593828a37d21654d8ea6e05d6484964d56cc861f Mon Sep 17 00:00:00 2001 From: Igor Bogoslavskyi Date: Wed, 6 May 2026 22:37:04 +0200 Subject: [PATCH 03/10] Almost final text --- lectures/std_function.md | 55 ++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/lectures/std_function.md b/lectures/std_function.md index 034dc3c..deaa97a 100644 --- a/lectures/std_function.md +++ b/lectures/std_function.md @@ -9,15 +9,15 @@ - [Type Erasure (How it works under the hood)](#type-erasure-how-it-works-under-the-hood) - [Summary](#summary) -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. +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. -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. +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! ## Overview -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. +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! -## The problem: Storing Callables -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. +## The problem: storing callables +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. 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: @@ -29,8 +29,8 @@ Because we want our code to be extremely fast and flexible, we decide to use **t template class Button { public: - explicit Button(std::string name, Callback cb) - : name_{std::move(name)}, on_click_{std::move(cb)} {} + explicit Button(std::string name, Callback callback) + : name_{std::move(name)}, on_click_{std::move(callback)} {} void Click() const { 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 **Problem 1: Heterogeneous Collections** 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! + + @@ -70,12 +72,12 @@ std::vector> buttons{play_button, quit_button}; ``` **Problem 2: Multiple Mixed Callbacks** -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 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. +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 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. -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! +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! ## Enter `std::function` -To solve these problems we can use `std::function`, introduced in C++11 and available in the `` 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. +To solve all of these problems we can use `std::function`, introduced in C++11 and available in the `` 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. The syntax for `std::function` is straightforward: + From 81727867fe071b29862b9e2f4d8aadf071f42de6 Mon Sep 17 00:00:00 2001 From: Igor Bogoslavskyi Date: Tue, 19 May 2026 21:32:18 +0200 Subject: [PATCH 04/10] Final text --- lectures/std_function.md | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/lectures/std_function.md b/lectures/std_function.md index deaa97a..115033b 100644 --- a/lectures/std_function.md +++ b/lectures/std_function.md @@ -1,15 +1,14 @@ -# `std::function` +`std::function` -- - - [`std::function`](#stdfunction) - [Overview](#overview) -- [The problem: Storing Callables](#the-problem-storing-callables) +- [The problem: storing callables](#the-problem-storing-callables) - [Enter `std::function`](#enter-stdfunction) - [Performance considerations](#performance-considerations) - [Type Erasure (How it works under the hood)](#type-erasure-how-it-works-under-the-hood) - [Summary](#summary) -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. +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. 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! @@ -72,9 +71,9 @@ std::vector> buttons{play_button, quit_button}; ``` **Problem 2: Multiple Mixed Callbacks** -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 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. +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 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. -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! +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! ## Enter `std::function` To solve all of these problems we can use `std::function`, introduced in C++11 and available in the `` 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 { private: std::string name_; // ✅ We can store a list of ANY callables! - std::vector> on_click_callbacks_; + std::vector> on_click_callbacks_; }; 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 Let's look at the pros and cons of each of the two approaches we just explored. **Template Approach:** -* **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. -* **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. -* **Con**: As we saw, we cannot easily store these objects in a single standard container, because they all have different underlying types. + +- **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. +- **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. +- **Con**: As we saw, we cannot easily store these objects in a single standard container, because they all have different underlying types. **`std::function` Approach:** -* **Pro**: True dynamic, runtime polymorphism! The `Button` is a single, concrete class. It can accept *any* callable matching the signature at runtime. -* **Pro**: Because all matching callables are wrapped in the exact same type (e.g., `std::function`), we can easily store them in vectors, pass them around, and swap out their callbacks on the fly. -* **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. -* **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! + +- **Pro**: True dynamic, runtime polymorphism! The `Button` is a single, concrete class. It can accept *any* callable matching the signature at runtime. +- **Pro**: Because all matching callables are wrapped in the exact same type (e.g., `std::function`), we can easily store them in vectors, pass them around, and swap out their callbacks on the fly. +- **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. +- **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! 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. @@ -257,7 +258,7 @@ class MyFunction { T stored_callable; }; - // 3. We only store a pointer to the base class. + // 3. We only store a pointer to the base class. // The exact type T is erased! std::unique_ptr callable_; }; @@ -284,7 +285,7 @@ This is the essence of Type Erasure. The `MyFunction` class itself is *not* temp 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! -> ⚠️ 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)! +> ⚠️ 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)! ## Summary 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. From d93ee6e0eddc57594c03bca04c000281f825ff70 Mon Sep 17 00:00:00 2001 From: Igor Bogoslavskyi Date: Wed, 20 May 2026 23:10:23 +0200 Subject: [PATCH 05/10] Final text + initial animation --- animation/projects/package-lock.json | 4 +- .../projects/src/std_function/project.meta | 32 +++++ .../projects/src/std_function/project.ts | 9 ++ .../src/std_function/scenes/std_function.meta | 5 + .../src/std_function/scenes/std_function.tsx | 88 ++++++++++++ animation/projects/vite.config.ts | 1 + lectures/std_function.md | 126 +++++++++++------- 7 files changed, 216 insertions(+), 49 deletions(-) create mode 100644 animation/projects/src/std_function/project.meta create mode 100644 animation/projects/src/std_function/project.ts create mode 100644 animation/projects/src/std_function/scenes/std_function.meta create mode 100644 animation/projects/src/std_function/scenes/std_function.tsx diff --git a/animation/projects/package-lock.json b/animation/projects/package-lock.json index 7a61f91..7b0bb53 100644 --- a/animation/projects/package-lock.json +++ b/animation/projects/package-lock.json @@ -1,11 +1,11 @@ { - "name": "parallelism", + "name": "projects", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "parallelism", + "name": "projects", "version": "0.0.0", "dependencies": { "@lezer/cpp": "^1.1.2", diff --git a/animation/projects/src/std_function/project.meta b/animation/projects/src/std_function/project.meta new file mode 100644 index 0000000..e02e8dc --- /dev/null +++ b/animation/projects/src/std_function/project.meta @@ -0,0 +1,32 @@ +{ + "version": 0, + "shared": { + "background": "rgb(10,20,30)", + "range": [ + 0, + null + ], + "size": { + "x": 1920, + "y": 1080 + }, + "audioOffset": 0 + }, + "preview": { + "fps": 30, + "resolutionScale": 1 + }, + "rendering": { + "fps": 60, + "resolutionScale": 1, + "colorSpace": "srgb", + "exporter": { + "name": "@motion-canvas/core/image-sequence", + "options": { + "fileType": "image/png", + "quality": 100, + "groupByScene": false + } + } + } +} \ No newline at end of file diff --git a/animation/projects/src/std_function/project.ts b/animation/projects/src/std_function/project.ts new file mode 100644 index 0000000..487161e --- /dev/null +++ b/animation/projects/src/std_function/project.ts @@ -0,0 +1,9 @@ +import { makeProject } from '@motion-canvas/core'; + +import '../../global.css'; + +import std_function from './scenes/std_function?scene'; + +export default makeProject({ + scenes: [std_function], +}); diff --git a/animation/projects/src/std_function/scenes/std_function.meta b/animation/projects/src/std_function/scenes/std_function.meta new file mode 100644 index 0000000..25c7da9 --- /dev/null +++ b/animation/projects/src/std_function/scenes/std_function.meta @@ -0,0 +1,5 @@ +{ + "version": 0, + "timeEvents": [], + "seed": 3221513550 +} \ No newline at end of file diff --git a/animation/projects/src/std_function/scenes/std_function.tsx b/animation/projects/src/std_function/scenes/std_function.tsx new file mode 100644 index 0000000..05432fc --- /dev/null +++ b/animation/projects/src/std_function/scenes/std_function.tsx @@ -0,0 +1,88 @@ +import { makeScene2D, Code, LezerHighlighter, lines } from '@motion-canvas/2d'; +import { all, createRef, waitFor } from '@motion-canvas/core'; +import { MyStyle } from '../../styles'; +import { parser as parser_cpp } from '@lezer/cpp'; +import { DEFAULT } from '@motion-canvas/core/lib/signals'; +import { centerOn } from '../../utils'; + +import templateCode from '@lectures/std_function.md?snippet=std_function/template_button.cpp'; +import stdFunctionCode from '@lectures/std_function.md?snippet=std_function/std_function_button.cpp'; +import multiButtonCode from '@lectures/std_function.md?snippet=std_function/multi_button.cpp'; +import typeErasureCode from '@lectures/std_function.md?snippet=std_function/type_erasure.cpp'; + +const CppHighlighter = new LezerHighlighter(parser_cpp, MyStyle); + +export default makeScene2D(function* (view) { + const codeRef = createRef(); + + view.add( + + ); + + // Initial delay + yield* waitFor(1); + + // 1. Template Button + yield* centerOn(codeRef(), lines(4, 5), 1, 40); // template + yield* waitFor(3); + + yield* centerOn(codeRef(), lines(17, 17), 1, 40); // Callback on_click_; + yield* waitFor(3); + + yield* centerOn(codeRef(), DEFAULT, 1, 28); + yield* waitFor(2); + + // 3. std::function Button + yield* codeRef().code(templateCode, 0); + yield* centerOn(codeRef(), DEFAULT, 0, 24); + yield* waitFor(1); + yield* codeRef().code(stdFunctionCode, 1); + yield* waitFor(1); + + yield* centerOn(codeRef(), lines(8, 9), 1, 40); // explicit Button(std::string name, std::function callback) + yield* waitFor(3); + + yield* centerOn(codeRef(), lines(18, 18), 1, 40); // std::function on_click_; + yield* waitFor(3); + + yield* centerOn(codeRef(), lines(30, 30), 1, 40); // std::vector