sigs, my simple thread-safe signal/slot C++ library, has been extended to support slot functions with non-void return types. Such return values can be gathered by triggering the signal with a function. But the argument type must be the same as the return type!
The following example adds together the integers from each connected slot:
sigs::Signal<int()> s;
s.connect([] { return 1; });
s.connect([] { return 2; });
s.connect([] { return 3; });
int sum = 0;
s([&sum](int retVal) { sum += retVal; });
// sum is now = 1 + 2 + 3 = 6
This enables a way for each slot to contribute to a shared computation.
In order to implement this, the Signal::operator(..)
must be able to assert that the provided function either has no argument, representing a slot with no return type, or a non-void argument for a slot with one. Enter VoidableFunction
:
template <typename T>
class VoidableFunction {
public:
using func = std::function<void(T)>;
};
/// Specialization for void return types.
template <>
class VoidableFunction<void> {
public:
using func = std::function<void()>;
};
template <typename Ret, typename... Args>
class Signal<Ret(Args...)> {
using Slot = std::function<Ret(Args...)>;
// ..
public:
using ReturnType = Ret;
// ..
template <typename RetFunc = typename VoidableFunction<ReturnType>::func>
void operator()(const RetFunc &retFunc, Args &&... args)
{
// ..
A specialization was necessary for void return types due to the lack of a value: a value is only be provided in the non-void case, which is what typename RetFunc = typename VoidableFunction<ReturnType>::func
yields.
The isVoidReturn()
is used to statically assert at compile-time that no void return type is used:
template <typename RetFunc = typename VoidableFunction<ReturnType>::func>
void operator()(const RetFunc &retFunc, Args &&... args)
{
static_assert(!isVoidReturn(), "Must have non-void return type!");
// ..
}
private:
static constexpr bool isVoidReturn()
{
return std::is_void<ReturnType>::value;
}
This is necessary since the prototype must match both void and non-void types such that it can yield a sensible error in the former case.
For each connected slot, the return value function is called with the result of the slot function, or passed to chained signals:
Lock lock(entriesMutex);
for (auto &entry : entries) {
auto *sig = entry.signal();
if (sig) {
(*sig)(retFunc, std::forward<Args>(args)...);
}
else {
retFunc(entry.slot()(std::forward<Args>(args)...));
}
}