Better_Software_Header_MobileBetter_Software_Header_Web

Find what you need - explore our website and developer resources

Loose Coupling with Signals & Slots

Connecting (Almost) Any Function to a Signal

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);
commandSignal.connect(std::bind(&Window::setTitle, &appWindow, std::placeholders::_1));
// 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);
// 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>();
}
template<typename Return, typename Class, typename... Arguments>
constexpr size_t get_arity(TypeMarker<Return (Class::*)(Arguments...)>)
{
    return sizeof...(Arguments) + 1;
}
// 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)
// 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)...)
        )
    );
}
Signal<std::string, std::chrono::time_point<std::chrono::system_clock>> commandSignal; 

Window appWindow;
commandSignal.connect(&Window::setTitle, &window);
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);

About KDAB