std::function- Overview
- The problem: storing callables
- Enter
std::function - Performance considerations
- Type Erasure (How it works under the hood)
- Summary
Remember in one of our previous lectures we looked at lambdas? We saw how they give us an easy and clean way to create function objects on the fly, saving us from writing boilerplate structs just to pass a callable - for example, a simple comparator or an operation to a standard algorithm.
But what happens if we want to store these callables somewhere and call them at a later time? And what if we want these callables to be anything that behaves like a function, not just lambdas?
This setup is exactly where std::function comes to the rescue!
As always, let's start with an illustrative example. Imagine we are building a UI framework, and we want to create a Button class. A button doesn't have to know what should happen when it is clicked, it just needs to know that it has been clicked. The user of our Button object should be able to hook up any logic they want by passing in a callback - a callable that is called when an action takes place, in our case, when the button gets clicked.
One thing we could do is to use templates. If we template the entire Button class on the type of the callable, we can store the provided callback as a member variable, say, on_click_ and call it when the click occurs:
#include <iostream>
#include <string>
template <typename Callback>
class Button {
public:
Button(std::string name, Callback callback)
: name_{std::move(name)}, on_click_{std::move(callback)} {}
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_play = [] { std::cout << "Playing game!\n"; };
const Button play_button{"Play", lambda_play};
const Button quit_button{"Quit", QuitGame};
play_button.Click();
quit_button.Click();
}With this approach, we can create, say a "play" button and a "quit" button that each calls their own callback. It's not a bad approach!
Look how our templated Button handles the different types of callables seamlessly. We don't even need to provide the template type as it is inferred using Class Template Argument Deduction! Here we pass a lambda into the "play" button callback and a free standing function into the "quit" button callback.
If we just want to create these buttons and use them in isolation, like in our example, this approach works wonderfully! On top of everything else, 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.
Problem 1: Heterogeneous Collections
For example, a UI framework usually needs a way to draw all buttons on the screen or handle their input in a loop. So we would like to 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 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!
template<>
class Button<__lambda_23_22> {
// Specialization code generated by the compiler
};
template<>
class Button<void(*)()> {
// Specialization code generated by the compiler
};
// ❌ Won't compile. What is the type of the vector?
// play_button and quit_button are DIFFERENT class types!
std::vector<Button<???>> 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<Callback> on_click_callbacks_. But again, because std::vector demands a single type for all of its elements, 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, I hear some of you ask? Well, technically it would allow us to store various types of callables in a single vector, but some of those types would be horrible temporary anonymous classes generated by the compiler (like in the case of a lambda)! So we wouldn't be able to provide them to our variant upfront! Which brings us to the same problem: what type would a vector store?
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, move, 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, it is a template class templated on the return type as well as all the argument types in parentheses:
std::function<ReturnType(ArgumentType1, ArgumentType2, ...)>All of these can be references, pointers, const etc. just like any normal function arguments.
Let's rewrite our Button class using std::function then. We will remove the template from the class and instead use std::function<void()> for our callback (meaning "takes no arguments, returns void"). Let's start by keeping exactly one callback per button, matching our original example:
#include <functional>
#include <iostream>
#include <string>
#include <vector>
class Button {
public:
Button(std::string name, std::function<void()> callback)
: name_{std::move(name)}, on_click_{std::move(callback)} {}
void Click() const {
std::cout << "[" << name_ << "] was clicked.\n";
if (on_click_) { on_click_(); }
}
private:
std::string name_{};
std::function<void()> on_click_{};
};
void QuitGame() { std::cout << "Quitting game...\n"; }
int main() {
const auto lambda_play = [] { std::cout << "Playing game!\n"; };
const Button play_button{"Play", lambda_play};
const Button quit_button{"Quit", QuitGame};
// ✅ This works now! Both buttons are the EXACT SAME type.
const std::vector<Button> buttons{play_button, quit_button};
for (const auto& button : buttons) { button.Click(); }
}There are not that many changes really! The Button class is not templated anymore but takes a std::function<void()> callback as a parameter and stores this callback internally.
The main function also remains very similar, except now we can put our buttons in a vector.
When we run this, it works beautifully, calling both of our buttons' callbacks:
[Play] was clicked.
Playing game!
[Quit] was clicked.
Quitting game...
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:
#include <functional>
#include <iostream>
#include <string>
#include <vector>
class Button {
public:
Button(std::string name, std::vector<std::function<void()>> callbacks)
: name_{std::move(name)}, on_click_callbacks_{std::move(callbacks)} {}
void Click() const {
std::cout << "[" << name_ << "] was clicked.\n";
for (const auto& callback : on_click_callbacks_) {
if (callback) { callback(); }
}
}
private:
std::string name_{};
std::vector<std::function<void()>> on_click_callbacks_{};
};
void QuitGame() { std::cout << "Quitting game...\n"; }
int main() {
const Button play_button{"Play", {
[] { std::cout << "Playing game!\n"; },
[] { std::cout << "Logging: Play was clicked.\n"; },
}};
const Button quit_button{"Quit", {QuitGame}};
const std::vector<Button> buttons{play_button, quit_button};
for (const auto& button : buttons) { button.Click(); }
}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.
When we run the code, all of our callbacks are being called as expected:
[Play] was clicked.
Playing game!
Logging: Play was clicked.
[Quit] was clicked.
Quitting game...
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. That's why we check if (callback) before calling it, similar to how we can check if a pointer is valid before dereferencing it.
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.
Let's look at the pros and cons of each of the two approaches we just explored.
Template Approach:
- 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.
- 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.
- Con: Objects have different types so we can't store them in a single standard container.
std::function Approach:
- Pro: The
std::functioncan wrap any callable matching the provided signature at runtime. - 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. - Con: Heap Allocation. Implementations of
std::functionusually 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::functionwill callnewto allocate memory on the heap. This can be a major performance bottleneck if used on a hot path. - Con: Virtual Call Overhead. Invoking a
std::functiongenerally involves an indirect function call (like calling a virtual function), which can make it harder for the compiler to perform all of its optimizations.
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::sort taking a templated callable as a parameter
template<class RandomIt, class Compare>
void sort(RandomIt first, RandomIt last, Compare comp);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.
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!
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?
This is achieved using a design pattern called type erasure.
💡 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, includingstd:shared_ptr,std::any, and, as we'll see, instd::functionitself.
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.
The core idea is to use a templated class that inherits from a non-templated base class that uses virtual functions to call the stored callable:
#include <iostream>
#include <memory>
class MyFunction {
public:
// Other constructors missing for brevity
template <typename T>
MyFunction(T&& callable)
: callable_{std::make_unique<CallableImpl<T>>(std::forward<T>(callable))} {}
void operator()() const { if (callable_) { callable_->Invoke(); } }
private:
// See Inheritance lecture for Noncopyable definition.
struct CallableBase : public Noncopyable {
virtual ~CallableBase() = default;
virtual void Invoke() const = 0;
};
template <typename T>
struct CallableImpl : public CallableBase {
explicit CallableImpl(T callable) : stored_callable{std::move(callable)} {}
void Invoke() const override { stored_callable(); }
T stored_callable;
};
std::unique_ptr<CallableBase> callable_{};
};
void FreeFunction() { std::cout << "Free function!\n"; }
int main() {
const MyFunction func1{FreeFunction};
const MyFunction func2{[] { std::cout << "Lambda!\n"; }};
func1();
func2();
}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 for more details.
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.
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.
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!
And of course, if we run the code we get the printout that we expect!
Free function!
Lambda!
Real std::function class 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 some years ago, looking at you, Misha)!
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 is extremely versatile, allowing to use any callable object with the same interface.
However, with great power comes great responsibility (and potential heap allocations) as well as a potential runtime overhead due to the use of a vtable.
So, as a rule of thumb, we should probably use std::function for callbacks, event handlers, and storing tasks, but stick to templates when we need maximum performance on hot paths, unless we measured otherwise.
