Home The simplest std::function explanation in 15 minutes
Post
Cancel

The simplest std::function explanation in 15 minutes

This post was written under influence of a cool video from Jason Turner “A simplified implementation of std::function”

Often people don’t think about how std::function works. Most people know that this thing is a wrapper over something that can be “called” as a function. Some people vaguely remember that std::function somehow mess with memory on the heap. The cppreference does not reveal much about internals of its implementation.

We can say that there are two types of objects in C++ on which have the semantics of “calling as a function”. We can name them Callable. These two types are:

  1. Functions itself:
    1
    
    int foo(int a, int b) { return a + b; }
    
  2. Objects of types that have operator(), often they called “functors”:
    1
    2
    3
    
    struct foo {
     int operator()(int a, int b) { return a + b; }
    };
    

All other Callables are derived from these two types. In particular, lambdas is the second type: the compiler converts them into structures with operator(). I once reviewed a good book about lambdas in this blog.

So, a std::function<Signature> object is supposed to be able to hold any Callable with the given Signature.

1
2
3
4
template<typename Ret, typename... Param>
class function<Ret(Param...)> {
    // implementation
};

There is a problem - a std::function type should have a fixed size, but a Callable of type can have an unknown size. For example, the size of a lambda structure depends on which captures it does.

Therefore, unfortunately, a std::function object stores the Callable in the heap.

An implementation also needs to use tricks like using a virtual class, which will calculate the address of the function to call independently for each individual type. They would hold a pointer to this class:

1
2
3
4
5
    struct callable_interface {
        virtual Ret call(Param...) = 0;
        virtual ~callable_interface() = default;
    };
    std::unique_ptr<callable_interface> callable_ptr;

Its derived class for a given Callable holds a Callable object itself and overrides the method to call the right function:

1
2
3
4
5
6
    template<typename Callable>
    struct callable_impl : callable_interface {
        callable_impl(Callable callable_) : callable{std::move(callable_)} {}
        Ret call(Param... param) override { return std::invoke(callable, param...); };
        Callable callable;
    }

A std::function constructor takes a Callable and makes an object in the heap:

1
2
3
4
    template<typename Callable>
    function(Callable callable)
        : callable_ptr{std::make_unique<callable_impl<Callable>>(std::move(callable))}
    {}

Finally, calling the opetator() dispatches to the right place:

1
    Ret operator()(Param... param) { return callable_ptr->call(param...); }

This is how type erasure in C++ can look like 👍

This post is licensed under CC BY 4.0 by the author.

Helping the compiler with [[assume]]

The most vexing C++ rule for modular projects (and how to deal with it)