From bc8e01df4286f5f8ff9103fbebad801f355db689 Mon Sep 17 00:00:00 2001
From: Alexey Rusakov <Kitsune-Ral@users.sf.net>
Date: Sun, 3 Jul 2022 22:14:13 +0200
Subject: Shorten switchOnType, function_traits and connect*

...thanks to C++20 awesomeness. A notable change is that
wrap_in_function() (and respectively function_traits<>::function_type)
and fn_return_t alias are gone. The former are no more needed because
connectUntil/connectSingleShot no more use std::function. The latter
has been relatively underused and with the optimisation of switchOnType
hereby, could be completely replaced with std::invoke_result_t.

Rewriting connect* functions using constexpr and auto parameters made
the implementation 30% more compact and much easier to understand
(though still with a couple of - now thoroughly commented - tricky
places). Dropping std::function<> from it may also bring some (quite
modest, likely) performance benefits.
---
 lib/events/event.h       |  64 +++++++------------
 lib/function_traits.cpp  |   3 +
 lib/function_traits.h    |  36 +++++------
 lib/qt_connection_util.h | 160 +++++++++++++++++------------------------------
 4 files changed, 101 insertions(+), 162 deletions(-)

(limited to 'lib')

diff --git a/lib/events/event.h b/lib/events/event.h
index ec21c6aa..a966e613 100644
--- a/lib/events/event.h
+++ b/lib/events/event.h
@@ -341,56 +341,34 @@ inline auto eventCast(const BasePtrT& eptr)
                                            : nullptr;
 }
 
-// A trivial generic catch-all "switch"
-template <class BaseEventT, typename FnT>
-inline auto switchOnType(const BaseEventT& event, FnT&& fn)
-    -> decltype(fn(event))
-{
-    return fn(event);
-}
-
 namespace _impl {
-    // Using bool instead of auto below because auto apparently upsets MSVC
-    template <class BaseT, typename FnT>
-    constexpr bool needs_downcast =
-        std::is_base_of_v<BaseT, std::decay_t<fn_arg_t<FnT>>>
-        && !std::is_same_v<BaseT, std::decay_t<fn_arg_t<FnT>>>;
-}
-
-// A trivial type-specific "switch" for a void function
-template <class BaseT, typename FnT>
-inline auto switchOnType(const BaseT& event, FnT&& fn)
-    -> std::enable_if_t<_impl::needs_downcast<BaseT, FnT>
-                        && std::is_void_v<fn_return_t<FnT>>>
-{
-    using event_type = fn_arg_t<FnT>;
-    if (is<std::decay_t<event_type>>(event))
-        fn(static_cast<event_type>(event));
+    template <typename FnT, class BaseT>
+    concept Invocable_With_Downcast =
+        std::derived_from<std::remove_cvref_t<fn_arg_t<FnT>>, BaseT>;
 }
 
-// A trivial type-specific "switch" for non-void functions with an optional
-// default value; non-voidness is guarded by defaultValue type
-template <class BaseT, typename FnT>
-inline auto switchOnType(const BaseT& event, FnT&& fn,
-                         fn_return_t<FnT>&& defaultValue = {})
-    -> std::enable_if_t<_impl::needs_downcast<BaseT, FnT>, fn_return_t<FnT>>
+template <class BaseT, typename TailT>
+inline auto switchOnType(const BaseT& event, TailT&& tail)
 {
-    using event_type = fn_arg_t<FnT>;
-    if (is<std::decay_t<event_type>>(event))
-        return fn(static_cast<event_type>(event));
-    return std::move(defaultValue);
+    if constexpr (std::is_invocable_v<TailT, BaseT>) {
+        return tail(event);
+    } else if constexpr (_impl::Invocable_With_Downcast<TailT, BaseT>) {
+        using event_type = fn_arg_t<TailT>;
+        if (is<std::decay_t<event_type>>(event))
+            return tail(static_cast<event_type>(event));
+        return std::invoke_result_t<TailT, event_type>(); // Default-constructed
+    } else { // Treat it as a value to return
+        return std::forward<TailT>(tail);
+    }
 }
 
-// A switch for a chain of 2 or more functions
-template <class BaseT, typename FnT1, typename FnT2, typename... FnTs>
-inline std::common_type_t<fn_return_t<FnT1>, fn_return_t<FnT2>>
-switchOnType(const BaseT& event, FnT1&& fn1, FnT2&& fn2, FnTs&&... fns)
+template <class BaseT, typename FnT1, typename... FnTs>
+inline auto switchOnType(const BaseT& event, FnT1&& fn1, FnTs&&... fns)
 {
     using event_type1 = fn_arg_t<FnT1>;
     if (is<std::decay_t<event_type1>>(event))
-        return fn1(static_cast<event_type1&>(event));
-    return switchOnType(event, std::forward<FnT2>(fn2),
-                        std::forward<FnTs>(fns)...);
+        return fn1(static_cast<event_type1>(event));
+    return switchOnType(event, std::forward<FnTs>(fns)...);
 }
 
 template <class BaseT, typename... FnTs>
@@ -405,8 +383,8 @@ inline auto visit(const BaseT& event, FnTs&&... fns)
 // TODO: replace with ranges::for_each once all standard libraries have it
 template <typename RangeT, typename... FnTs>
 inline auto visitEach(RangeT&& events, FnTs&&... fns)
-    -> std::enable_if_t<std::is_void_v<
-        decltype(switchOnType(**begin(events), std::forward<FnTs>(fns)...))>>
+    requires std::is_void_v<
+        decltype(switchOnType(**begin(events), std::forward<FnTs>(fns)...))>
 {
     for (auto&& evtPtr: events)
         switchOnType(*evtPtr, std::forward<FnTs>(fns)...);
diff --git a/lib/function_traits.cpp b/lib/function_traits.cpp
index 6542101a..e3d27122 100644
--- a/lib/function_traits.cpp
+++ b/lib/function_traits.cpp
@@ -7,6 +7,9 @@
 
 using namespace Quotient;
 
+template <typename FnT>
+using fn_return_t = typename function_traits<FnT>::return_type;
+
 int f_();
 static_assert(std::is_same_v<fn_return_t<decltype(f_)>, int>,
               "Test fn_return_t<>");
diff --git a/lib/function_traits.h b/lib/function_traits.h
index 83b8e425..143ed162 100644
--- a/lib/function_traits.h
+++ b/lib/function_traits.h
@@ -8,7 +8,7 @@
 namespace Quotient {
 
 namespace _impl {
-    template <typename AlwaysVoid, typename>
+    template <typename>
     struct fn_traits {};
 }
 
@@ -21,73 +21,73 @@ namespace _impl {
  */
 template <typename T>
 struct function_traits
-    : public _impl::fn_traits<void, std::remove_reference_t<T>> {};
+    : public _impl::fn_traits<std::remove_reference_t<T>> {};
 
 // Specialisation for a function
 template <typename ReturnT, typename... ArgTs>
 struct function_traits<ReturnT(ArgTs...)> {
     using return_type = ReturnT;
     using arg_types = std::tuple<ArgTs...>;
-    // See also the comment for wrap_in_function() in qt_connection_util.h
-    using function_type = std::function<ReturnT(ArgTs...)>;
 };
 
 namespace _impl {
-    template <typename AlwaysVoid, typename>
+    template <typename>
     struct fn_object_traits;
 
     // Specialisation for a lambda function
     template <typename ReturnT, typename ClassT, typename... ArgTs>
-    struct fn_object_traits<void, ReturnT (ClassT::*)(ArgTs...)>
+    struct fn_object_traits<ReturnT (ClassT::*)(ArgTs...)>
         : function_traits<ReturnT(ArgTs...)> {};
 
     // Specialisation for a const lambda function
     template <typename ReturnT, typename ClassT, typename... ArgTs>
-    struct fn_object_traits<void, ReturnT (ClassT::*)(ArgTs...) const>
+    struct fn_object_traits<ReturnT (ClassT::*)(ArgTs...) const>
         : function_traits<ReturnT(ArgTs...)> {};
 
     // Specialisation for function objects with (non-overloaded) operator()
     // (this includes non-generic lambdas)
     template <typename T>
-    struct fn_traits<decltype(void(&T::operator())), T>
-        : public fn_object_traits<void, decltype(&T::operator())> {};
+    requires requires { &T::operator(); }
+    struct fn_traits<T>
+        : public fn_object_traits<decltype(&T::operator())> {};
 
     // Specialisation for a member function in a non-functor class
     template <typename ReturnT, typename ClassT, typename... ArgTs>
-    struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...)>
+    struct fn_traits<ReturnT (ClassT::*)(ArgTs...)>
         : function_traits<ReturnT(ClassT, ArgTs...)> {};
 
     // Specialisation for a const member function
     template <typename ReturnT, typename ClassT, typename... ArgTs>
-    struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...) const>
+    struct fn_traits<ReturnT (ClassT::*)(ArgTs...) const>
         : function_traits<ReturnT(const ClassT&, ArgTs...)> {};
 
     // Specialisation for a constref member function
     template <typename ReturnT, typename ClassT, typename... ArgTs>
-    struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...) const&>
+    struct fn_traits<ReturnT (ClassT::*)(ArgTs...) const&>
         : function_traits<ReturnT(const ClassT&, ArgTs...)> {};
 
     // Specialisation for a prvalue member function
     template <typename ReturnT, typename ClassT, typename... ArgTs>
-    struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...) &&>
+    struct fn_traits<ReturnT (ClassT::*)(ArgTs...) &&>
         : function_traits<ReturnT(ClassT&&, ArgTs...)> {};
 
     // Specialisation for a pointer-to-member
     template <typename ReturnT, typename ClassT>
-    struct fn_traits<void, ReturnT ClassT::*>
+    struct fn_traits<ReturnT ClassT::*>
         : function_traits<ReturnT&(ClassT)> {};
 
     // Specialisation for a const pointer-to-member
     template <typename ReturnT, typename ClassT>
-    struct fn_traits<void, const ReturnT ClassT::*>
+    struct fn_traits<const ReturnT ClassT::*>
         : function_traits<const ReturnT&(ClassT)> {};
 } // namespace _impl
 
-template <typename FnT>
-using fn_return_t = typename function_traits<FnT>::return_type;
-
 template <typename FnT, int ArgN = 0>
 using fn_arg_t =
     std::tuple_element_t<ArgN, typename function_traits<FnT>::arg_types>;
 
+template <typename FnT>
+constexpr auto fn_arg_count_v =
+    std::tuple_size_v<typename function_traits<FnT>::arg_types>;
+
 } // namespace Quotient
diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h
index 86593cc8..7477273f 100644
--- a/lib/qt_connection_util.h
+++ b/lib/qt_connection_util.h
@@ -9,101 +9,68 @@
 
 namespace Quotient {
 namespace _impl {
-    template <typename... ArgTs>
-    using decorated_slot_tt =
-        std::function<void(QMetaObject::Connection&, const ArgTs&...)>;
+    enum ConnectionType { SingleShot, Until };
 
-    template <typename SenderT, typename SignalT, typename ContextT, typename... ArgTs>
-    inline QMetaObject::Connection
-    connectDecorated(SenderT* sender, SignalT signal, ContextT* context,
-                     decorated_slot_tt<ArgTs...> decoratedSlot,
-                     Qt::ConnectionType connType)
+    template <ConnectionType CType>
+    inline auto connect(auto* sender, auto signal, auto* context, auto slotLike,
+                        Qt::ConnectionType connType)
     {
-        auto pc = std::make_unique<QMetaObject::Connection>();
-        auto& c = *pc; // Resolve a reference before pc is moved to lambda
-
-        // Perfect forwarding doesn't work through signal-slot connections -
-        // arguments are always copied (at best - COWed) to the context of
-        // the slot. Therefore the slot decorator receives const ArgTs&...
-        // rather than ArgTs&&...
-        // TODO (C++20): std::bind_front() instead of lambda.
-        c = QObject::connect(sender, signal, context,
-            [pc = std::move(pc),
-             decoratedSlot = std::move(decoratedSlot)](const ArgTs&... args) {
-                Q_ASSERT(*pc); // If it's been triggered, it should exist
-                decoratedSlot(*pc, args...);
+        std::unique_ptr<QMetaObject::Connection> pConn =
+            std::make_unique<QMetaObject::Connection>();
+        auto& c = *pConn; // Save the reference before pConn is moved from
+        c = QObject::connect(
+            sender, signal, context,
+            [slotLike, pConn = std::move(pConn)](const auto&... args)
+            // The requires-expression below is necessary to prevent Qt
+            // from eagerly trying to fill the lambda with more arguments
+            // than slotLike() (i.e., the original slot) can handle
+            requires requires { slotLike(args...); } {
+                static_assert(CType == Until || CType == SingleShot,
+                              "Unsupported disconnection type");
+                if constexpr (CType == SingleShot) {
+                    // Disconnect early to avoid re-triggers during slotLike()
+                    QObject::disconnect(*pConn);
+                    // Qt kindly keeps slot objects until they do their job,
+                    // even if they disconnect themselves in the process (see
+                    // how doActivate() in qobject.cpp handles c->slotObj).
+                    slotLike(args...);
+                } else if constexpr (CType == Until) {
+                    if (slotLike(args...))
+                        QObject::disconnect(*pConn);
+                }
             },
             connType);
         return c;
     }
-    template <typename SenderT, typename SignalT, typename ContextT,
-              typename... ArgTs>
-    inline QMetaObject::Connection
-    connectUntil(SenderT* sender, SignalT signal, ContextT* context,
-                 std::function<bool(ArgTs...)> functor,
-                 Qt::ConnectionType connType)
-    {
-        return connectDecorated(sender, signal, context,
-            decorated_slot_tt<ArgTs...>(
-                [functor = std::move(functor)](QMetaObject::Connection& c,
-                                               const ArgTs&... args) {
-                    if (functor(args...))
-                        QObject::disconnect(c);
-                }),
-            connType);
-    }
-    template <typename SenderT, typename SignalT, typename ContextT,
-              typename... ArgTs>
-    inline QMetaObject::Connection
-    connectSingleShot(SenderT* sender, SignalT signal, ContextT* context,
-                      std::function<void(ArgTs...)> slot,
-                      Qt::ConnectionType connType)
-    {
-        return connectDecorated(sender, signal, context,
-            decorated_slot_tt<ArgTs...>(
-                [slot = std::move(slot)](QMetaObject::Connection& c,
-                                         const ArgTs&... args) {
-                    QObject::disconnect(c);
-                    slot(args...);
-                }),
-            connType);
-    }
 
-    // TODO: get rid of it as soon as Apple Clang gets proper deduction guides
-    //       for std::function<>
-    //       ...or consider using QtPrivate magic used by QObject::connect()
-    //       ...for inspiration, also check a possible std::not_fn implementation
-    //       at https://en.cppreference.com/w/cpp/utility/functional/not_fn
-    template <typename FnT>
-    inline auto wrap_in_function(FnT&& f)
-    {
-        return typename function_traits<FnT>::function_type(std::forward<FnT>(f));
-    }
+    template <typename SlotT, typename ReceiverT>
+    concept PmfSlot =
+        (fn_arg_count_v<SlotT> > 0
+         && std::derived_from<ReceiverT, std::decay_t<fn_arg_t<SlotT, 0>>>);
 } // namespace _impl
 
-/*! \brief Create a connection that self-disconnects when its "slot" returns true
- *
- *  A slot accepted by connectUntil() is different from classic Qt slots
- * in that its return value must be bool, not void. The slot's return value
- * controls whether the connection should be kept; if the slot returns false,
- * the connection remains; upon returning true, the slot is disconnected from
- * the signal. Because of a different slot signature connectUntil() doesn't
- * accept member functions as QObject::connect or Quotient::connectSingleShot
- * do; you should pass a lambda or a pre-bound member function to it.
- */
-template <typename SenderT, typename SignalT, typename ContextT, typename FunctorT>
-inline auto connectUntil(SenderT* sender, SignalT signal, ContextT* context,
-                         const FunctorT& slot,
+//! \brief Create a connection that self-disconnects when its slot returns true
+//!
+//! A slot accepted by connectUntil() is different from classic Qt slots
+//! in that its return value must be bool, not void. Because of that different
+//! signature connectUntil() doesn't accept member functions in the way
+//! QObject::connect or Quotient::connectSingleShot do; you should pass a lambda
+//! or a pre-bound member function to it.
+//! \return whether the connection should be dropped; false means that the
+//!         connection remains; upon returning true, the slot is disconnected
+//!         from the signal.
+inline auto connectUntil(auto* sender, auto signal, auto* context,
+                         auto smartSlot,
                          Qt::ConnectionType connType = Qt::AutoConnection)
 {
-    return _impl::connectUntil(sender, signal, context, _impl::wrap_in_function(slot),
-                               connType);
+    return _impl::connect<_impl::Until>(sender, signal, context, smartSlot,
+                                        connType);
 }
 
-/// Create a connection that self-disconnects after triggering on the signal
-template <typename SenderT, typename SignalT, typename ContextT, typename FunctorT>
-inline auto connectSingleShot(SenderT* sender, SignalT signal,
-                              ContextT* context, const FunctorT& slot,
+//! Create a connection that self-disconnects after triggering on the signal
+template <typename ContextT, typename SlotT>
+inline auto connectSingleShot(auto* sender, auto signal, ContextT* context,
+                              SlotT slot,
                               Qt::ConnectionType connType = Qt::AutoConnection)
 {
 #if QT_VERSION_MAJOR >= 6
@@ -111,25 +78,16 @@ inline auto connectSingleShot(SenderT* sender, SignalT signal,
                             Qt::ConnectionType(connType
                                                | Qt::SingleShotConnection));
 #else
-    return _impl::connectSingleShot(
-        sender, signal, context, _impl::wrap_in_function(slot), connType);
-}
-
-// Specialisation for usual Qt slots passed as pointers-to-members.
-template <typename SenderT, typename SignalT, typename ReceiverT,
-          typename SlotObjectT, typename... ArgTs>
-inline auto connectSingleShot(SenderT* sender, SignalT signal,
-                              ReceiverT* receiver,
-                              void (SlotObjectT::*slot)(ArgTs...),
-                              Qt::ConnectionType connType = Qt::AutoConnection)
-{
-    // TODO: when switching to C++20, use std::bind_front() instead
-    return _impl::connectSingleShot(sender, signal, receiver,
-                                    _impl::wrap_in_function(
-                                        [receiver, slot](const ArgTs&... args) {
-                                            (receiver->*slot)(args...);
-                                        }),
-                                    connType);
+    // In case for usual Qt slots passed as pointers-to-members the receiver
+    // object has to be pre-bound to the slot to make it self-contained
+    if constexpr (_impl::PmfSlot<SlotT, ContextT>) {
+        return _impl::connect<_impl::SingleShot>(sender, signal, context,
+                                                 std::bind_front(slot, context),
+                                                 connType);
+    } else {
+        return _impl::connect<_impl::SingleShot>(sender, signal, context, slot,
+                                                 connType);
+    }
 #endif
 }
 
-- 
cgit v1.2.3