Loose Coupling with Signals & Slots Connecting (Almost) Any Function to a Signal
Here at KDAB, we recently published a library called KDBindings, which aims to reimplement both Qt signals and slots and data binding in pure C++17.
To get an introduction to the KDBindings implementation of signals and slots, I recommend that you take a look at the KDBindings Getting Started Guide. It will give you an overview of what signals and slots are, as well as how our implementation of them is used. Alternatively, take a look at our introductory blog post.
On a basic level, signals model an event that might occur, like a command being issued or a variable being set. The signal will then “emit” the data associated with the event. This data is the signal’s “signature,” similar to a normal function signature. The signal can then be connected to any function with a matching signature. Such functions are referred to as “slots.” Whenever the signal is emitted, it will call all of these slots in an arbitrary order. They can then react to the event.
Because this mechanism is very well-suited to loosely couple different systems, we discovered that it is often useful to connect a signal to a slot, even if their signatures only partially match.
This is especially useful if the signature of the signal was not designed to explicitly work together with this particular slot, but rather to be generic. Signals might emit too much information, for example, when a slot is only interested in the occurrence of an event and not the data associated with it. On the other hand, a slot might require additional information that is not emitted by the signal but might be available when the slot is connected.
Commands and Titles
Let’s say we have a Signal<std::string, std::chrono::time_point<std::chrono::system_clock>>
that is emitted every time the user issues a command. The std::string
that is emitted contains the command issued and the time_point
contains the timestamp of when the command was issued.
Now imagine that we want to update the title of our application so that it always displays the last issued command.
Ideally, we would want to be able to write something like this to make that happen:
class Window {
public:
void setTitle(const std::string& title) { ... }
};
// This Signal would be emitted every time the user issues a command
Signal<std::string, std::chrono::time_point<std::chrono::system_clock>> commandSignal;
Window appWindow;
commandSignal.connect(&Window::setTitle, &appWindow);
Unfortunately, this won’t just work immediately. The connect
function on the signal needs to do a lot of work here:
Window::setTitle
is a member function, so it will need athis
pointer to the object it should modify. In this case, this is&appWindow
.- The function doesn’t have any use for the
time_point
emitted by the signal, only for thestd::string
. So, the signal must discard its second argument for this particular slot.
std::bind Function Pointer as a Workaround
The problem is that the default implementation of the connect function only takes a “std::function<void(std::string, std::chrono::time_point<std::chrono::system_clock>)>” as its argument.
A workaround for this could be the use of std::bind
:
commandSignal.connect(std::bind(&Window::setTitle, &appWindow, std::placeholders::_1));
With this, we can provide our member function with the required pointer to our appWindow
. Furthermore, std::bind returns an object that can be called with a basically unlimited number of arguments. Since we only used std::placeholders::_1
, it will then discard all arguments except the first one. In our case, this would discard the time_point
argument, which is exactly what we need.
In general, using std::bind
would work. However, the previously shown API would be a lot nicer, as it hides the std::bind
call and doesn’t require the use of any placeholders.
Let’s use template meta-programming to write a function that can generate the needed call to std::bind
for us.
The bind_first Function
Our initial goal is to create a function that, similar to std::bind
, can bind arguments to a function. However, for our use case, we only want to bind the first arguments and refrain from explicitly specifying the placeholders for every remaining argument.
For the general idea of how to solve this problem, I found a Github Gist that defines a function bind_first
that almost does what we need. It can generate the needed placeholders for std::bind
as well as forward the arguments to be bound.
The problem with this implementation is that it uses sizeof…(Args) to determine how many placeholders need to be generated. The Args variadic template argument is only available because it requires the function to be a non-const member function. So, this won’t work on just any function type. It won’t even work on const member functions.
However, if we assume there is a compile-time function get_arity
that returns the arity (number of arguments) of a function, we can improve bind_first
to accept any function type:
// We create a template struct that can be used instead of std::placeholders::_X.
// Then we can use template-meta-programming to generate a sequence of placeholders.
template<int>
struct placeholder {
};
// To be able to use our custom placeholder type, we can specialize std::is_placeholder.
namespace std {
template<int N>
struct is_placeholder<placeholder<N>>
: integral_constant<int, N> {
};
}; // namespace std
// This helper function can then bind a certain number of arguments "Args",
// and generate placeholders for the rest of the arguments, given an
// index_sequence of placeholder numbers.
//
// Note the +1 here, std::placeholders are 1-indexed and the 1 offset needs
// to be added to the 0-indexed index_sequence.
template<typename Func, typename... Args, std::size_t... Is>
auto bind_first_helper(std::index_sequence<Is...>, Func &&fun, Args... args)
{
return std::bind(
std::forward<Func>(fun),
std::forward<Args>(args)...,
placeholder<Is + 1>{}...
);
}
// The bind_first function then simply generates the index_sequence by
// subtracting the number of arguments the function "fun" takes, with
// the number of arguments Args, that are to be bound.
template<
typename Func,
typename... Args,
/*
Disallow any placeholder arguments, they would mess with the number
and ordering of required and bound arguments, and are, for now, unsupported
*/
typename = std::enable_if_t<
std::conjunction_v<std::negation<std::is_placeholder<Args>>...>
>
>
auto bind_first(Func &&fun, Args &&...args)
{
return bind_first_helper(
std::make_index_sequence<get_arity<Func>() - sizeof...(Args)>{},
std::forward<Func>(fun),
std::forward<Args>(args)...
);
}
// An example use:
QWindow window;
std::function<void(QString, std::chrono::time_point<std::chrono::system_clock>)> bound =
bind_first(&QWindow::setTitle, &window);
The get_arity Function
As noted earlier, the bind_first
implementation assumes the existence of a get_arity
function, which can determine the number of arguments of a function at compile time.
This function is, however, not part of standard C++. So, how does one go about implementing it?
To understand the basic idea behind how this can work, take a look at this Stackoverflow post: https://stackoverflow.com/questions/27866909/get-function-arity-from-template-parameter.
In comparison to the StackOverflow answer, I chose to implement this using a constexpr function, as I find the interface clearer when called.
Without further ado, here’s the code:
// This is just a template struct necessary to overload the get_arity function by type.
// C++ doesn't allow partial template function specialization, but we can overload it with our tagged type.
template<typename T>
struct TypeMarker {
constexpr TypeMarker() = default;
};
// Base implementation of get_arity refers to specialized implementations for each
// type of callable object by using the overload for it's specialized TypeMarker.
template<typename T>
constexpr size_t get_arity()
{
return get_arity(TypeMarker<std::decay_t<T>>{});
}
// The arity of a function pointer is simply it's number of arguments.
template<typename Return, typename... Arguments>
constexpr size_t get_arity(TypeMarker<Return (*)(Arguments...)>)
{
return sizeof...(Arguments);
}
template<typename Return, typename... Arguments>
constexpr size_t get_arity(TypeMarker<Return (*)(Arguments...) noexcept>)
{
return sizeof...(Arguments);
}
// The arity of a generic callable object is the arity of it's operator() - 1,
// as the "this" pointer is already known for such an object.
// As lambdas are also just instances of an anonymous class, they must also implement
// the operator() member function, so this also works for lambdas.
template<typename T>
constexpr size_t get_arity(TypeMarker<T>)
{
return get_arity(TypeMarker<decltype(&T::operator())>{}) - 1;
}
// Syntactic sugar version of get_arity, allows passing any callable object
// to get_arity, instead of having to pass its decltype as a template argument.
template<typename T>
constexpr size_t get_arity(const T &)
{
return get_arity<T>();
}
This code will work for free functions. It also works for arbitrary objects that implement the function call operator (operator()
) by delegating to the get_arity
function for that operator()
member function. This also includes lambdas, as they must implement the function call operator as well.
Unfortunately, implementing get_arity
for member functions is a bit more complicated.
An initial implementation might look like this:
template<typename Return, typename Class, typename... Arguments>
constexpr size_t get_arity(TypeMarker<Return (Class::*)(Arguments...)>)
{
return sizeof...(Arguments) + 1;
}
This works well for normal, plain member functions. What about const-correctness though?
We would need another overload for TypeMarker<Return (Class::*)(Arguments...) const>
. Taking a look at the cppreference for function declarations reveals that we also need to take care of volatile
, noexcept
, as well as ref qualified (&
and &&
) member functions.
Unfortunately, I have not found any way to remove these specifiers from the function pointer types that are already available. But instantiating every combination isn’t too bad when we use a macro to do the heavy lifting for us:
// remember to add +1, the "this" pointer is an implicit argument
#define KDBINDINGS_DEFINE_MEMBER_GET_ARITY(MODIFIERS) template<typename Return, typename Class, typename... Arguments> \
constexpr size_t get_arity(TypeMarker<Return (Class::*)(Arguments...) MODIFIERS>) \
{ \
return sizeof...(Arguments) + 1; \
}
KDBINDINGS_DEFINE_MEMBER_GET_ARITY()
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(const)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(&)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(const &)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(&&)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(const &&)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile const)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile &)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile const &)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile &&)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile const &&)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(noexcept)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(const noexcept)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(&noexcept)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(const &noexcept)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(&&noexcept)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(const &&noexcept)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile noexcept)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile const noexcept)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile &noexcept)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile const &noexcept)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile &&noexcept)
KDBINDINGS_DEFINE_MEMBER_GET_ARITY(volatile const &&noexcept)
So it’s a macro to generate template functions, which are pretty much macros themselves — that’s a lot of meta programming.
But in the end, we are successful. This code will work for (almost) any callable object. The current limits I can think of are overloaded functions as well as template functions. These have to be disambiguated, as it’s not clear which overload get_arity
should refer to.
The definition of get_arity might especially come in handy in the future, if further metaprogramming with arbitrary callable objects is needed.
Putting It All Together
With our completed bind_first
implementation, we can now define a new connect function for our signal:
// We use the enable_if_t here to disable this function if the argument is already
// convertible to the correct std::function type
template<typename Func, typename... FuncArgs>
auto connect(Func &&slot, FuncArgs &&...args) const
-> std::enable_if_t<
std::disjunction_v<
std::negation<std::is_convertible<Func, std::function<void(Args...)>>>,
/* Also enable this function if we want to bind at least one argument*/
std::integral_constant<bool, sizeof...(FuncArgs)>>,
ConnectionHandle
>
{
return connect(
static_cast<std::function<void(Args...)>>(
bind_first(std::forward<Func>(slot),
std::forward<FuncArgs>(args)...)
)
);
}
And, finally, we have our desired API:
Signal<std::string, std::chrono::time_point<std::chrono::system_clock>> commandSignal;
Window appWindow;
commandSignal.connect(&Window::setTitle, &window);
As mentioned earlier, this also allows us to bind functions to a signal that need additional information.
Signal<std::string, std::chrono::time_point<std::chrono::system_clock>> commandSignal;
// Imagine we had a logging function that takes a logging level
// and a message to log.
void log(const LogLevel& level, const std::string& message);
// Connecting this function will now log the user command every time
// with the log level "Info".
commandSignal.connect(log, LogLevel::Info);
All of these features come together to make KDBindings signals very flexible in how they can connect to signals. That makes it very easy to couple together systems without having to make the signals and slots entirely line up first.
KDBindings
If you want to check out the complete code, take a look at the KDBindings source on Github.
The library also offers a lot more awesome features, like properties and data binding in pure C++17. It’s a header-only library that is easy to integrate and licensed with the very liberal MIT-License.
This, of course, also means that you’re free to use the code shown here however you like.
If you like this article and want to read similar material, consider subscribing via our RSS feed.
Subscribe to KDAB TV for similar informative short video content.
KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.