Introduction
Recently, a chat with a friend peeked my interested: how would you store an arbitrary function and call it, similar to std::function<>
. It turned out a plain C function pointer would suffice for this specific use-case, but I got triggered: let’s implement a generic, move-only function wrapper in C++!
What about std::function<> ?
First of all, we won’t be re-implementing std::function<>
as it requires the function to be copyable, which is not always desirable: a copy could be very expensive, or even not possible at all. This is why C++23 introduced std::move_only_function<>
. Recently, a proposal for std::copyable_function<>
was voted into C++26, which seeks to replace std::function<>
with a more explicit and more const-correct alternative. Another proposal seeks to deprecate std::function<>
, and use std::move_only_function<>
and std::copyable_function<>
instead.
As for our implementation, it will be closer to std::move_only_function<>
than std::function<>
. We’ll approach the problem piece-by-piece, and fill out details as they become important. Let’s go!
Tell me about std::move_only_function<>
The idea is that you can store any movable function, for example:
std::move_only_function<int(int, int)> fn =
[](int a, int b) { return a + b; };
return fn(1, 2); // 3
Because it only needs to be movable, you can have the lambda take ownership of move-only objects:
auto i = std::make_unique<int>(3);
std::move_only_function<int(int, int)> fn =
[i = std::move(i)](int a, int b) { return a + b + *i; };
// ^ 'i' will now be owned by the lambda stored in 'fn'
return fn(1, 2); // 6
Getting started
Let’s start by solving a simpler problem: let’s assume whatever function we are going to manage takes two int
parameters, and has a int
return type (we’ll lift this limitation later – this is mainly to avoid more templates). Because every lambda has a unique type, we need to use a catch-all type Func
. We start with the following:
class MyFunction
{
template<typename Func>
MyFunction(Func function)
{
/* ... store function somewhere ... */
}
int operator()(int a, int b)
{
/* ... call stored function somehow ... */
}
};
Ideally, we’d store Func
directly inside our MyFunction
class, as a member. But this cannot work: Func
can be any type (remember that every lambda has its own unique type), and since the size may differ depending on how much is captured, we cannot know the size up front.
Taking a step back, rather than storing the type of Func
itself, we rather want to store any function which fulfills the int fn(int, int)
prototype. In other words, the actual type of Func
is not really relevant for us: as long as whatever we store provides an int operator()(int, int)
function, it will suffice. This is the interface to which whatever implements the storage must provide.
Erasing the type
Let’s define the interface that we want to call:
struct MyFunctionInterface
{
virtual ~MyFunctionInterface() = default;
virtual int operator()(int, int) = 0;
};
Using this interface, we can create an implementation, which works for any invokable function:
template<typename Fn>
class MyFunctionImpl : public MyFunctionInterface
{
Fn fn;
public:
MyFunctionImpl(Fn fn) : fn(std::move(fn)) { }
int operator()(int a, int b) override
{
return fn(a, b);
}
};
MyFunctionImpl
implements MyFunctionInterface
, and will just call operator()
in fn
. This allows us to do the following:
MyFunctionInterface* f =
new MyFunctionImpl([] (int a, int b) { return a + b; });
return f->operator()(1, 2); // 3
Note that the actual lambda type does not appear anywhere anymore! This is known as type erasure and is the key to implement generic callable functions such as std::move_only_function<>
and others.
Back to our function wrapper
Given that we now have a way to store any function that fulfills the int fn(int, int)
prototype, we can implement our function wrapper as follows:
class MyFunction
{
std::unique_ptr<MyFunctionInterface> fn;
public:
template<typename Func>
MyFunction(Func function) {
fn = std::make_unique<MyFunctionImpl<Func>>(
std::move(function)
);
}
int operator()(int a, int b) {
return fn->operator()(a, b);
}
};
We get some function of type Func
in our constructor, and use MyFunctionImpl<Func>
to store this implementation, which will type-erase it.
Let’s try whether this works!
MyFunction fn([] (int a, int b) { return a + b; });
return fn(1, 2); // 3
Awesome!
Next time, let’s see how we can make it accept any function prototype, instead of only int fn(int, int)
.