Introduction
Previously, we’ve seen a way to implement our own version of std::move_only_function<>
. The implementation we ended up with is as follows:
struct MyFunctionInterface
{
virtual ~MyFunctionInterface() = default;
virtual int operator()(int, int) = 0;
};
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);
}
};
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);
}
};
This works for any movable function, for example:
auto i = std::make_unique<int>(3);
MyFunction fn([c = std::move(i)] (int a, int b) {
return a + b + *c; });
// ^ i is moved from the caller into the lambda as c
return fn(1, 2); // 6
Unfortunately, it is restricted to the function prototype int fn(int, int)
– during this post, we’ll make the function able to return any type and take any arguments.
Getting started
It is easiest to work top-down for situations like these: what is the most specific place where the prototype is hardcoded? It is in MyFunctionInterface
, so let’s make that generic:
template<typename ReturnType, typename... Args>
struct MyFunctionInterface
{
virtual ReturnType operator()(Args...) = 0;
};
By changing all uses of MyFunctionInterface
to MyFunctionInterface<int, int, int>
the code compiles again.
Changing the type-erased implementation
We want to change our type-erased function implementation MyFunctionImpl
: instead of just taking the stored function Fn
type, we need both the return type and the argument types. Right now, we have:
template<typename Fn>
class MyFunctionImpl : public MyFunctionInterface<int, int, int>
{
Fn fn;
public:
MyFunctionImpl(Fn fn) : fn(std::move(fn)) { }
int operator()(int a, int b) override
{
return fn(a, b);
}
};
Our first step is to add return type and argument types as template arguments:
template<typename Fn, typename ReturnType, typename... Args>
class MyFunctionImpl : public MyFunctionInterface<ReturnType, Args...>
{
/* ... */
};
This won’t yet compile: we need to alter the instantiation of MyFunctionImpl
as well:
template<typename Func>
MyFunction(Func function)
{
fn = std::make_unique<MyFunctionImpl<Func, int, int, int>>(
std::move(function));
}
As a last step, we update the call operator as follows:
ReturnType operator()(Args... args) override
{
return fn(args...);
}
This will also work if ReturnType
is void
: you can return from a function returning void
as long as the expression result evaluates to void
. This is very convenient for generic code.
Moving towards the function wrapper
We are almost there yet: we need to remove the hardcoded types from MyFunction
. Currently, we have the following:
class MyFunction
{
std::unique_ptr<MyFunctionInterface<int, int, int>> fn;
public:
template<typename Func>
MyFunction(Func function)
{
fn = std::make_unique<MyFunctionImpl<Func, int, int, int>>(
std::move(function));
}
int operator()(int a, int b)
{
return fn->operator()(a, b);
}
};
We start by adding template arguments and updating the type-erased function’s type:
template<typename ReturnType, typename... Args>
class MyFunction
{
std::unique_ptr<MyFunctionInterface<ReturnType, Args...>> fn;
public:
template<typename Func>
MyFunction(Func function)
{
fn = std::make_unique<MyFunctionImpl<Func, ReturnType, Args...>>(
std::move(function));
}
/* ... */
};
As MyFunction
is now a template and the compiler can’t inference the return type, we update our example:
MyFunction<int, int, int> fn([c = std::move(i)] (int a, int b) {
return a + b + *c;
});
We need to update our calling operator to benefit from the generic types:
ReturnType operator()(Args... args)
{
return fn->operator()(args...);
}
We’ve removed all notion of int in our MyFunction! However, our type is MyFunction<int, int, int>
instead of std::move_only_function<int(int, int)>
. We aren’t done yet!
Making the type more readable and accessible
std::function<int(int, int)>
– and the other function wrappers – use int(int, int)
in a clever and readable way to separate the return type from the argument types. In order to achieve this, we use a template specialization:
template<typename ReturnType, typename... Args>
class MyFunction;
template<typename ReturnType, typename... Args>
class MyFunction<ReturnType(Args...)>
{
/* ...
};
The first declaration introduces MyFunction
as a template with the named types, and then we specialize it using for instantiations matching ReturnType(Args..)
. This allows us to change our invocation as follows:
auto i = std::make_unique<int>(3);
MyFunction<int(int, int)> fn([c = std::move(i)] (int a, int b) {
return a + b + *c;
});
return fn(1, 2); // 6
And we are done! Well… almost!
Handling movable only parameters
The following example doesn’t yet work with our implementation:
MyFunction<int(std::unique_ptr<int>)> pfn([] (auto v) {
return *v + *v;
});
return pfn(std::make_unique<int>(9));
This doesn’t compile because our current implementation will attempt to copy the std::unique_ptr<int>
. We need to make sure it passes the Args
exactly as they are supplied when forwarding to the call operator. This is where std::forward<>
comes in.
We need to update all calls as follows:
template<typename ReturnType, typename... Args>
class MyFunction<ReturnType(Args...)>
{
/* ... */
ReturnType operator()(Args... args) {
return fn->operator()(std::forward<Args>(args)...);
}
};
template<typename Fn, typename ReturnType, typename... Args>
class MyFunctionImpl : public MyFunctionInterface<ReturnType, Args...>
{
/* ... */
ReturnType operator()(Args... args) override
{
return fn(std::forward<Args>(args)...);
}
};
This ensures the arguments are passed exactly as supplied, removing the potential copies. This allows non-copyable types to be used as arguments. Amazing!
Next time, in the final part of the series, let’s take a look how we can avoid the dynamic memory allocation and instead use a fixed-sized buffer to store the function.