Implementing a std::function<>-like wrapper in C++, part 1: type erasing

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).

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 *