Creating a Fast and Efficient Delegate Type (Part 2)
Upgrading Delegate to C++17
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>();
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
{
...
}
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:
- We can get the type-deduction of
Class
for free, and - 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());
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!");
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.