Creating a Fast and Efficient Delegate Type (Part 1)
A simple solution to lightweight function binding
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:
- a pointer to the instance (if one exists), and
- 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: template
s! More specifically:
non-type template
s.
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);)
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());
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 const
ness 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());
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.