Implementing a std::function<>-like wrapper in C++, part 2: generalizing the return type and arguments

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.

This entry was posted in Programming and tagged . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *