Creating a Fast and Efficient Delegate Type (Part 1)

A simple solution to lightweight function binding

  • 10 minutes to read

When working in C++ systems, a frequent design pattern that presents itself is the need to bind and reuse functions in a type-erased way. Whether it’s returning a callback from a function, or enabling support for binding listeners to an event (such as through a signal or observer pattern), this is a pattern that can be found everywhere.

In many cases, especially when working in more constrained systems such as an embedded system, these bound functions will often be nothing more than small views of existing functions – either as just a function pointer, or as a coupling of this with a member function call. In almost all cases, the function being bound is already known at compile-time. Very seldomly does a user need to bind an opaque function (such as the return value from a call to ::dlsym ).

Although the C++ standard does provide some utilities for type-erased callbacks such as std::function or std::packaged_task , neither of these solutions provide any standards-guaranteed performance characteristics, and neither of these are guaranteed to be optimized for the above suggested cases.

We can do better. Lets set out to produce a better alternative to the existing solutions that could satisfy this problem in a nice and coherent way.

Goal

To create a fast, light-weight alternative to std::function that operates on non-owning references. For our purposes, we will name this type Delegate.

The criteria we will focus on in this post will be to be able to bind functions to this type at compile-time in a way that works for c++11 and above

Over the next few posts, we will iterate on this design to make it even better by introducing covariance and 0-overhead.

Basic Structure

The most obvious start for this is to create a class template that works with any function signature. Since we need to know both the type of the return and the type of the arguments in order to invoke the function, we will want to use a partial specialization that allows us to extract this information.

// Primary template intentionally left empty
template <typename Signature>
class Delegate;

// Partial specialization so we can see the return type and arguments
template <typename R, typename...Args>
class Delegate<R(Args...)>
{
public:
  // Creates an unbound delegate
  Delegate() = default;

  // We want the Delegate to be copyable, since its lightweight
  Delegate(const Delegate& other) = default;
  auto operator=(const Delegate& other) -> Delegate& = default;

  ...

  // Call the underlying bound function
  auto operator()(Args...args) const -> R;

private:
  ...
};

As with any type-erasure, we need to think of how we will intend to normalize any input into a Delegate<R(Args...)>.

Keeping in mind that this should support both raw function pointers and bound member functions with a class instance, we will need to be able to store at least two pieces of data:

  1. a pointer to the instance (if one exists), and
  2. a function pointer to perform the invocation

Remembering that a member function pointer is conceptually similar to a function pointer that takes a this pointer as the first instance – lets model that with R(*)(This*, Args...)!

But what should the type of This be? There is no one type for this if we are binding any class object. Raw pointers don’t even have a this pointer at all!

Lets solve this with the simplest, most C++ way we can think of: const void*. This can be nullptr for raw function pointers that have no this, or it can be a pointer to the instance for bound member functions! Then we can simply cast the type back to the correct This pointer as needed – keep it simple!

template <typename R, typename...Args>
class Delegate<R(Args...)>
{
  ...
private:
  using stub_function = R(*)(const void*, Args...);

  const void* m_instance = nullptr; ///< A pointer to the instance (if it exists)
  stub_function m_stub = nullptr;   ///< A pointer to the function to invoke
};

Great! Now we just need some way of generating these stub functions.

Invoking the Delegate

Before we go to far, lets implement the invocation of operator() with m_instance.

Since we know that we want to erase a possible instance pointer to be m_instance, and our stub is m_stub – all we are really doing is calling the m_stub bound function and passing m_instance as the first argument, forwarding the rest along.

However, we will want to make sure we don’t accidentally call this while m_stub is nullptr, since that would be undefined behavior. Lets throw an exception in such a case:

class BadDelegateCall : public std::exception { ... };

template <typename R, typename...Args>
class Delegate<R(Args...)>
{
public:
  ...

  auto operator()(Args...args) const -> R
  {
    if (m_stub == nullptr) {
      throw BadDelegateCall{};
    }
    return (*m_stub)(m_instance, args...);
  }

  ...
};

Okay, now before we can actually call this, we will need to find a way to generate proper m_stub functions and call them

Generating Stubs

So how can we create a stub function from the actual function we want to bind?

Keeping in mind that we want this solution to work only for functions known at compile-time gives us the answer: templates! More specifically: non-type templates.

Function stubs

So lets take a first crack at how we can create a stub for non-member functions. We need a function pointer that has the same signature as our stub pointer, and we need to hide the real function we want to call in a template non-type argument.

Keep in mind that because we want a regular function pointer, we will want this function to be marked static so that it’s not actually a member function of the class (which would create a pointer-to-member-function, which is not the same as a function pointer).

So lets do this by making a static function template, that will simply invoke the bound function pointer with the specified arguments:

template <typename R, typename...Args>
class Delegate<R(Args...)>
{
  ...
private:
  ...

  /// A Stub function for free functions
  template <R(*Function)(Args...)>
  static auto nonmember_stub(const void* /* unused */, Args...args) -> R
  {
    return (*Function)(args...);
  }

  ...
};

Now we have something that models the stub function, so we just need a way to actually bind this to the delegate. Lets do this with a simple bind function:

template <typename R, typename...Args>
class Delegate<R(Args...)>
{
public:
  ...

  template <R(*Function)(Args...)>
  auto bind() -> void
  {
    // We don't use this for non-member functions, so just set it to nullptr
    m_instance = nullptr;
    // Bind the function pointer
    m_stub = &nonmember_stub<Function>;
  }

  ...
};

Perfect; now we have a means of binding free functions. But it turns out there is an even simpler way so that we can avoid having to write a static function at all – and the answer is lambdas.

One really helpful but often unknown feature of non-capturing lambdas is that they are convertible to a function pointer of the same signature. This allows us to avoid wiring a whole separate function template:

template <typename R, typename...Args>
class Delegate<R(Args...)>
{
public:
  ...

  template <R(*Function)(Args...)>
  auto bind() -> void
  {
    m_instance = nullptr;
    m_stub = static_cast<stub_function>([](const void*, Args...args) -> R {
      return (*Function)(args...);
    });
  }

  ...
};

Lets give this a quick test for the sake of sanity:

auto square(int x) -> int { return x * x; }

auto d = Delegate<int(int)>{};
d.bind<&square>();

assert(d(2) == 4);)
Try Online

Excellent – we have something that works. Now onto member functions.

Member Function Stubs

Member function stubs are a little more complicated – because now we have to work with pointer-to-member syntax and take into account both const and non-const variations.

The general syntax for a pointer-to-member function is R(Class::*)(Args...) Where R is the return type, Args... are the arguments, and Class is the type that we are taking the member of.

So how can we get this into a stub?

The first problem you might notice is that it is not possible to use the same syntax of c.bind<&Foo::do_something>() – and this is due to the Class type now being part of the signature:

template <typename R, typename...Args>
class Delegate<R(Args...)>
{
public:
  ...

  template <R(Class::*)(Args...) const>
  //          ^~~~~
  //        Where do we get the type name 'Class' from?
  auto bind(const Class* c) -> void { ... }

  ...
};

We still need to find a way to name the Class type as a template parameter. The simplest possibility is for us to just add a typename parameter for Class. Lets see what happens if we do that:

template <typename R, typename...Args>
class Delegate<R(Args...)>
{
public:
  ...

  template <typename Class, R(Class::*)(Args...) const>
  auto bind(const Class* c) -> Delegate;

  ...
};

Good – so now we have a well-formed template. But notice anything different?

By adding typename Class as the first parameter, we can no longer simply call c.bind<&Foo::do_something>() because the pointer is no longer the first parameter! This effectively changes the call to now be: c.bind<Foo,&Foo::do_Something>().

It turns out that there is nothing, prior to c++17, that we can do about this because we need to have some way of naming the Foo type first. This can however be done with C++17 using auto parameters (see part 2 of this series for more details).

So for now we will have to use the above approach for binding.

Binding const member functions

Lets start with adding the binding support for const member functions. This will be very similar to our function implementation, except now we finally get to use the const void* parameter:

template <typename R, typename...Args>
class Delegate<R(Args...)>
{
public:
  ...

  template <typename Class, R(Class::*MemberFunction)(Args...) const>
  auto bind(const Class* c) -> void {
    m_instance = c; // store the class pointer
    m_stub = static_cast<stub_function>([](const void* p, Args...args) -> R {
      const auto* cls = static_cast<const Class*>(p);

      return (cls->*MemberFunction)(args...);
    });
  }

  ...
};

We finally get to use the p parameter and simply cast it back to the const Class* type. This is safe because we know that this was what we bound immediately on the line before.

Lets try this now, using std::string and trying to bind std::string::size:

auto str = std::string{"Hello"};

auto d = Delegate<std::string::size_type()>{};
d.bind<std::string, &std::string::size>(&str);

assert(d() == str.size());
Try Online

Excellent – it works! Now we just need to do the same for non-const members

Binding non-const member functions

The non-const version will look very similar to the const version. You might notice one issue from the initial design: our m_instance member is a const void*, but now our member pointers are not const! How can we work around this?

It turns out, this is one of the few cases where a const_cast is actually the perfect solution. We know that the only m_instance pointer we bind to this will be non-const by the time we bound it, so it’s completely safe for us to remove this constness again. After all, we did only add const so that we had an easy heterogeneous type to convert to.

Lets see what this looks like:

template <typename R, typename...Args>
class Delegate<R(Args...)>
{
public:
  ...
  template <typename Class, R(Class::*MemberFunction)(Args...)>
  auto bind(Class* c) -> void {
    m_instance = c; // store the class pointer
    m_stub = static_cast<stub_function>([](const void* p, Args...args) -> R {
      // Safe, because we know the pointer was bound to a non-const instance
      auto* cls = const_cast<Class*>(static_cast<const Class*>(p));

      return (cls->*MemberFunction)(args...);
    });
  }
  ...
};

This looks almost identical to our const version, except we have the one const_cast. Lets give this a quick test using std::string and std::string::clear as a quick example:

auto str = std::string{"hello"};

auto d = Delegate<void()>{};
d.bind<std::string, &std::string::clear>(&str);

d();
assert(str.empty());
Try Online

And now we have something that works that allows us to bind free, member, and const-member functions at compile-time!

Closing Remarks

There we have it. Using a small amount of templates, we were able to build a small, light-weight utility for calling functions bound at compile-time.

There is still much we can, and will do, to improve this design.

Check out part 2 to see how we can support covariance, and part 3 to see this optimizes to have zero-overhead.

Next Post