Creating a Fast and Efficient Delegate Type (Part 2)

Upgrading Delegate to C++17

  • 4 minutes to read

In the previous post , we saw how we could build a simple and light-weight Delegate type that binds free functions, and member functions. However we have a notable limitation that we require specifying the type of the members being bound (e.g. d.bind<Foo,&Foo::do_something>()). Additionally, we’re forced to bind only the exact type. We can’t bind anything that is covariant to it.

Lets improve upon that.

Goal

To improve upon our initial Delegate implementation from the previous post.

In particular, we will support both covariant functions and improve upon the bind function in the process. For this improvement, we will require C++17.

Supporting Covariance

Reworking Free-Function Binding

So how can we support covariance? To do this we need a non-type template parameter that supports any function pointer signature that may be similar.

This is where C++17’s auto-template parameters will play a huge role. auto parameters are non-type parameters that use auto-deduction semantics.

Lets try making this change. While we’re at it, since we’re using c++17, lets also update the function invocation to use std::invoke in the process:

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

With this, now the following code compiles:

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

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

Not bad for a minor improvement! However, notice that at the moment auto parameters are unconstrained, meaning that you could realistically call bind<2>() and this will fail spectacularly with some horrible template error. However, we can easily fix this by just constraining the template’s inputs by using SFINAE :

  template <auto Function,
            typename = std::enable_if_t<std::is_invocable_r_v<R, decltype(Function),Args...>>>
  auto bind() -> void
  {
    ...
  }
Try Online

This will now ensure that calling bind<2>() will error that there is no valid overload available, rather than failing with some complicated template error.

Reworking Const Member Functions

Now we need to support member functions using auto. This will allow us to support both covariance and remove the redundant type specification.

Unlike before where we had to order the template arguments with the Class first, auto arguments now allow this order to be reversed to be:

template <typename MemberFunction, typename Class>
auto bind(const Class* cls)

This now provides us with two things:

  1. We can get the type-deduction of Class for free, and
  2. We can use the desired calling notation of d.bind<&Foo::do_something>()

As with before, we will use SFINAE to ensure that this only works correctly with invocable functions, and std::invoke to clean up the code.

  template <auto MemberFunction, typename Class,
            typename = std::enable_if_t<std::is_invocable_r_v<R, decltype(MemberFunction),const Class*, Args...>>>
  auto bind(const Class* cls) -> void
  {
    // addressof used to ensure we get the proper pointer
    m_instance = cls;

    m_stub = static_cast<stub_function>([](const void* p, Args...args) -> R{
      // Cast back to the correct type
      const auto* c = static_cast<const Class*>(p);
      return std::invoke(MemberFunction, c, args...);
    });
  }

Lets check to make sure this works correctly:

auto str = std::string{"Hello"};
auto d = Delegate<long()>{};
d.bind<&std::string::size>(&str);

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

Excellent – we have it working, and with the desired syntax!

Lets do the same for non-const member functions.

Reworking Member Functions

The exact same change as we did for const member functions can be done for the non-const member function:

  template <auto MemberFunction, typename Class,
            typename = std::enable_if_t<std::is_invocable_r_v<R, decltype(MemberFunction),Class*, Args...>>>
  auto bind(Class* cls) -> void
  {
    m_instance = cls;
    m_stub = static_cast<stub_function>([](const void* p, Args...args) -> R{
      auto* c = const_cast<Class*>(static_cast<const Class*>(p));
      return std::invoke(MemberFunction, c, args...);
    });
  }

Lets again do a quick check to verify this works:

auto str = std::string{"Hello"};
auto d = Delegate<void(int)>{};
d.bind<&std::string::push_back>(&str);

d('!');

assert(str == "Hello!");
Try Online

And we have it working!

Closing Remarks

By making use of c++17 and auto parameters, we were able to enhance the functionality of our Delegate class to now support covariant functions and improve the user-experience by removing any redundant types from having to be specified – effectively making this utility even more useful!

And yet, there still is more we can improve on. In the next post I will cover optimizing this utility so that it has exactly 0-overhead over using raw pointers.

Next Post