structMyFunctionInterface{
virtual~MyFunctionInterface() =default;
virtualintoperator()(int, int) =0;
};
template<typename Fn>classMyFunctionImpl:public MyFunctionInterface
{
Fn fn;
public: MyFunctionImpl(Fn fn) : fn(std::move(fn)) { }
intoperator()(int a, int b) override {
return fn(a, b);
}
};
classMyFunction{
std::unique_ptr<MyFunctionInterface> fn;
public:template<typename Func> MyFunction(Func function)
{
fn = std::make_unique<MyFunctionImpl<Func>>(
std::move(function));
}
intoperator()(int a, int b)
{
return fn->operator()(a, b);
}
};
This works for any movable function, for example:
cpp
1
2
3
4
5
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
returnfn(1,2);// 6
1
2
3
4
5
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
returnfn(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:
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:
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:
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:
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:
cpp
1
2
3
4
5
auto i = std::make_unique<int>(3);MyFunction<int(int,int)> fn([c = std::move(i)](int a,int b){return a + b +*c;});returnfn(1,2);// 6
1
2
3
4
5
auto i = std::make_unique<int>(3);
MyFunction<int(int, int)> fn([c = std::move(i)] (int a, int b) {
return a + b +*c;
});
returnfn(1, 2); // 6
And we are done! Well… almost!
Handling movable only parameters
The following example doesn’t yet work with our implementation:
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.
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.