Morten's Dev

C++, Python, Rust, Emacs, Clang, CMake, and other technobabble..

Sigs: Return Values


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)...));
  }
}

Related Posts