Post

Type Erasure in C++: A callable reference

I regularly need to pass functions or callables around in my daily work. If you want to implement a work queue or if you’re going to implement an observer you will need to pass and store callable objects.

This is the fourth post in a series about type erasures:

What do we have?

In C++, you have some options to do this out of the box: function pointers, std::function, std::move_only_function1 or templates. All this options come with their advantages and drawbacks.2 For me, as an embedded C++ developer following points or important:

  1. easy to use and works with most callable objects
  2. small overhead (small object size, no allocations, no exceptions)
  3. no bloat and good to optimize

None of the provided options checks all criteria:

  • templates are not so easy to use and can potentially bloat the system3
  • function pointer can not handle most callable objects
  • std::function and std::move_only_function have a larger object size to allow small buffer optimization or need dynamic memory and are not easy to optimize by the compiler.

But gladly, there is another option: function_ref.

The Pattern

A function_ref (sometimes called function_view) is a lightweight non-owning generic callable object with reference semantics. This description makes it already similar to the non-ownwing type erasure described in the previous type erasure post. And the main idea is nearly identical: store the callable type erased and hold a function pointer to a lambda, which handles a type-safe cast based on a generic constructor.

The C++ standard does not yet provide a function_ref implementation, but it is already proposed. Lucky for us, a simple version is easy to implement.

At first, we need to enforce a function signature as a template parameter. We do this via partial template specialization. We need o define the template prototype:

1
2
template <typename>
class function_ref;

The prototype allows us to enforce a signature with a template specialization:

1
2
3
4
template <typename RETURN_T, typename... ARG_Ts>
class function_ref<RETURN_T(ARG_Ts...)> {
  /* ... */
};

The next step is to define our member parameters. We need a void* to store the address for our callable object: void* m_callable{nullptr};. We also need a pointer to a function that will receive our m_callable together with all function parameters:

1
2
using function_type = RETURN_T(void*, ARG_Ts...);
function_type* m_erased_fn{nullptr};

Now we need to define a generic constructor which initializes our member variables. m_callable will store the address of the provided callable, and m_erased_fn needs to point at a function that will cast our callable back in his original type and then calls it with all parameters:

1
2
3
4
5
6
7
8
9
template <typename CALLABLE_T>
    requires(!std::is_same_v<std::decay_t<CALLABLE_T>, function_ref>)
function_ref(CALLABLE_T&& callable) noexcept
    : m_callable{std::addressof(callable)}
    , m_erased_fn{[](void* ptr, ARG_Ts... args) -> RETURN_T {
          return (*static_cast<std::add_pointer_t<CALLABLE_T>>(ptr))(
              std::forward<ARG_Ts>(args)...);
      }} 
{}

The last step is to define our call operator. This operator only needs to call m_erased_fn with our stored callable (m_callable) and all necessary parameters:

1
2
3
RETURN_T operator()(ARG_Ts... args) const {
  return m_erased_fn(m_callable, std::forward<ARG_Ts>(args)...);
}

If we put all this together, it looks like this4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <memory>
#include <type_traits>

template <typename>
class function_ref;

template <typename RETURN_T, typename... ARG_Ts>
class function_ref<RETURN_T(ARG_Ts...)> {
   public:
    template <typename CALLABLE_T>
        requires(!std::is_same_v<std::decay_t<CALLABLE_T>, function_ref>)
    function_ref(CALLABLE_T&& callable) noexcept
        : m_callable{std::addressof(callable)}
        , m_erased_fn{[](void* ptr, ARG_Ts... args) -> RETURN_T {
              return (*static_cast<std::add_pointer_t<CALLABLE_T>>(ptr))(
                  std::forward<ARG_Ts>(args)...);
          }} 
    {}

    RETURN_T operator()(ARG_Ts... args) const {
        return m_erased_fn(m_callable, std::forward<ARG_Ts>(args)...);
    }

   private:
    using function_type = RETURN_T(void*, ARG_Ts...);
    void* m_callable{nullptr};
    function_type* m_erased_fn{nullptr};
};

The provided implementation is only an example and has some serious flaws and is only to illustrate the idea. If you want use it you should stick to an already existing implementation like zhihaoy/nontype_functional@p0792r13. or adapt the implementation. I described how a complete implementation can be done in a different blog post. If you want to do it on your own, this blog post gives a good overview what you should consider: Implementing function_view is harder than you might think

We can use it similar to the std::function implementation of the first type erasure post5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <vector>

struct dispatcher {
    void dispatch(const auto& msg) const noexcept {
        std::cout << "value: " << msg.value << '\n';
    }
};

struct int_message {
    int value = {};
};

struct float_message {
    float value = {};
};

int main() {
    using function_type = function_ref<void(const dispatcher&)>;

    std::vector<function_type> messages{[](const dispatcher& sink) { 
                                            int_message msg{.value = 42}; 
                                            sink.dispatch(msg);
                                        },
                                        [](const dispatcher& sink) { 
                                            float_message msg{.value = 3.14f}; 
                                            sink.dispatch(msg);
                                        },};

    dispatcher sink{};
    for (const auto& msg : messages) {
        msg(sink);
    }
}

In this code, we create a vector of our function_ref type, which stores the addresses of non-generic lambdas without capture and use the call operator for each of them inside the for loop.

Summary so far

A function_ref object provides a lightweight and efficient way to work with callable objects in C++. It is a non-owning alternative to std::function, which completely avoids unnecessary memory allocations, and still containing a small object size. These properties allow a significant performance improvement compared to std::function6 and make it a perfect tool in the toolbox for embedded development.

References

Blogs

Paper

Videos

Code

Footnotes

  1. std::move_only_function will be part of C++23 and was proposed in P0288 

  2. The proposal P0792 provides a good overview over the advantages and disadvantages. 

  3. Jason Turner wrote an article about this: template code bloat 

  4. This implementation is a slightly simplified version of llvm::function_ref 

  5. You can find the example in compiler explorer: https://godbolt.org/z/evorMPo58 

  6. You can see a benchmark in this blog post: passing functions to functions 

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