Преглед изворни кода

the delegate accepts now also functions with a shorter list of parameters

Michele Caini пре 6 година
родитељ
комит
ce00eee29f
3 измењених фајлова са 96 додато и 40 уклоњено
  1. 29 16
      docs/md/signal.md
  2. 52 24
      src/entt/signal/delegate.hpp
  3. 15 0
      test/entt/signal/delegate.cpp

+ 29 - 16
docs/md/signal.md

@@ -38,8 +38,9 @@ A delegate can be used as a general purpose invoker with no memory overhead for
 free functions and members provided along with an instance on which to invoke
 them.<br/>
 It does not claim to be a drop-in replacement for an `std::function`, so do not
-expect to use it whenever an `std::function` fits well. However, it can be used
-to send opaque delegates around to be used to invoke functions as needed.
+expect to use it whenever an `std::function` fits well. That said, it's most
+likely even a better fit than an `std::function` in a lot of cases, so expect to
+use it quite a lot anyway.
 
 The interface is trivial. It offers a default constructor to create empty
 delegates:
@@ -50,7 +51,7 @@ entt::delegate<int(int)> delegate{};
 
 All what is needed to create an instance is to specify the type of the function
 the delegate will _contain_, that is the signature of the free function or the
-member function one wants to assign to it.
+member one wants to assign to it.
 
 Attempting to use an empty delegate by invoking its function call operator
 results in undefined behavior or most likely a crash. Before to use a delegate,
@@ -75,7 +76,8 @@ delegate.connect<&my_struct::f>(&instance);
 
 The delegate class accepts also data members, if needed. In this case, the
 function type of the delegate is such that the parameter list is empty and the
-value of the data member is at least convertible to the return type.<br/>
+value of the data member is at least convertible to the return type.
+
 Functions having type equivalent to `void(T *, args...)` are accepted as well.
 In this case, `T *` is considered a payload and the function will receive it
 back every time it's invoked. In other terms, this works just fine with the
@@ -90,10 +92,25 @@ delegate(42);
 ```
 
 The function `g` will be invoked with a pointer to `c` and `42`. However, the
-function type of the delegate is still `void(int)`, mainly because this is also
-the signature of its function call operator.
+function type of the delegate is still `void(int)`. This is also the signature
+of its function call operator.
+
+Another interesting aspect of the delegate class is that it accepts also
+functions with a list of parameters that is shorter than that of the function
+type used to specialize the delegate itself.<br/>
+This is a nice-to-have feature in a lot of cases. The following code is
+therefore perfectly valid:
+
+```cpp
+void g() { /* ... */ }
+delegate.connect<&g>();
+delegate(42);
+```
 
-To create and initialize a delegate at once, there are also some specialized
+Where the function type of the delegate is `void(int)` as above. It goes without
+saying that the extra arguments are silently discarded internally.
+
+To create and initialize a delegate at once, there are a few specialized
 constructors. Because of the rules of the language, the listener is provided by
 means of the `entt::connect_arg` variable template:
 
@@ -113,26 +130,22 @@ if(delegate) {
 ```
 
 Finally, to invoke a delegate, the function call operator is the way to go as
-usual:
+already shown in the examples above:
 
 ```cpp
 auto ret = delegate(42);
 ```
 
-As shown above, listeners do not have to strictly follow the signature of the
+In all cases, the listeners don't have to strictly follow the signature of the
 delegate. As long as a listener can be invoked with the given arguments to yield
 a result that is convertible to the given result type, everything works just
 fine.
 
-Probably too much small and pretty poor of functionalities, but the delegate
-class can help in a lot of cases and it has shown that it is worth keeping it
-within the library.
-
 # Signals
 
 Signal handlers work with naked pointers, function pointers and pointers to
-member functions. Listeners can be any kind of objects and users are in charge
-of connecting and disconnecting them from a signal to avoid crashes due to
+member. Listeners can be any kind of objects and users are in charge of
+connecting and disconnecting them from a signal to avoid crashes due to
 different lifetimes. On the other side, performance shouldn't be affected that
 much by the presence of such a signal handler.<br/>
 A signal handler can be used as a private data member without exposing any
@@ -192,7 +205,7 @@ signal.sink().disconnect<&listener::bar>(&instance);
 signal.sink().disconnect();
 ```
 
-As shown above, listeners do not have to strictly follow the signature of the
+As shown above, the listeners don't have to strictly follow the signature of the
 signal. As long as a listener can be invoked with the given arguments to yield a
 result that is convertible to the given result type, everything works just fine.
 

+ 52 - 24
src/entt/signal/delegate.hpp

@@ -2,7 +2,9 @@
 #define ENTT_SIGNAL_DELEGATE_HPP
 
 
+#include <tuple>
 #include <cstring>
+#include <utility>
 #include <algorithm>
 #include <functional>
 #include <type_traits>
@@ -41,6 +43,20 @@ template<typename Class, typename Type>
 auto to_function_pointer(Type Class:: *, const Class *) -> Type(*)();
 
 
+template<typename>
+struct function_extent;
+
+
+template<typename Ret, typename... Args>
+struct function_extent<Ret(*)(Args...)> {
+    static constexpr auto value = sizeof...(Args);
+};
+
+
+template<typename Func>
+constexpr auto function_extent_v = function_extent<Func>::value;
+
+
 }
 
 
@@ -85,7 +101,37 @@ class delegate;
  */
 template<typename Ret, typename... Args>
 class delegate<Ret(Args...)> {
-    using proto_fn_type = Ret(const void *, Args...);
+    using proto_fn_type = Ret(const void *, std::tuple<Args...>);
+
+    template<auto Function, std::size_t... Index>
+    void connect(std::index_sequence<Index...>) ENTT_NOEXCEPT {
+        data = nullptr;
+
+        fn = +[](const void *, std::tuple<Args...> args) -> Ret {
+            static_assert(std::is_invocable_r_v<Ret, decltype(Function), std::tuple_element_t<Index, std::tuple<Args...>>...>);
+            // Ret(...) allows void(...) to eat return values and avoid errors
+            return Ret(std::invoke(Function, std::get<Index>(args)...));
+        };
+    }
+
+    template<auto Candidate, typename Type, std::size_t... Index>
+    void connect(Type *value_or_instance, std::index_sequence<Index...>) ENTT_NOEXCEPT {
+        data = value_or_instance;
+
+        fn = +[](const void *payload, std::tuple<Args...> args) -> Ret {
+            Type *curr = nullptr;
+
+            if constexpr(std::is_const_v<Type>) {
+                curr = static_cast<Type *>(payload);
+            } else {
+                curr = static_cast<Type *>(const_cast<void *>(payload));
+            }
+
+            static_assert(std::is_invocable_r_v<Ret, decltype(Candidate), Type *, std::tuple_element_t<Index, std::tuple<Args...>>...>);
+            // Ret(...) allows void(...) to eat return values and avoid errors
+            return Ret(std::invoke(Candidate, curr, std::get<Index>(args)...));
+        };
+    }
 
 public:
     /*! @brief Function type of the delegate. */
@@ -127,13 +173,8 @@ public:
      */
     template<auto Function>
     void connect() ENTT_NOEXCEPT {
-        static_assert(std::is_invocable_r_v<Ret, decltype(Function), Args...>);
-        data = nullptr;
-
-        fn = [](const void *, Args... args) -> Ret {
-            // this allows void(...) to eat return values and avoid errors
-            return Ret(std::invoke(Function, args...));
-        };
+        constexpr auto extent = internal::function_extent_v<decltype(internal::to_function_pointer(std::declval<decltype(Function)>()))>;
+        connect<Function>(std::make_index_sequence<extent>{});
     }
 
     /**
@@ -153,21 +194,8 @@ public:
      */
     template<auto Candidate, typename Type>
     void connect(Type *value_or_instance) ENTT_NOEXCEPT {
-        static_assert(std::is_invocable_r_v<Ret, decltype(Candidate), Type *, Args...>);
-        data = value_or_instance;
-
-        fn = [](const void *payload, Args... args) -> Ret {
-            Type *curr = nullptr;
-
-            if constexpr(std::is_const_v<Type>) {
-                curr = static_cast<Type *>(payload);
-            } else {
-                curr = static_cast<Type *>(const_cast<void *>(payload));
-            }
-
-            // this allows void(...) to eat return values and avoid errors
-            return Ret(std::invoke(Candidate, curr, args...));
-        };
+        constexpr auto extent = internal::function_extent_v<decltype(internal::to_function_pointer(std::declval<decltype(Candidate)>(), std::declval<Type *>()))>;
+        connect<Candidate>(value_or_instance, std::make_index_sequence<extent>{});
     }
 
     /**
@@ -204,7 +232,7 @@ public:
      */
     Ret operator()(Args... args) const {
         ENTT_ASSERT(fn);
-        return fn(data, args...);
+        return fn(data, std::forward_as_tuple(args...));
     }
 
     /**

+ 15 - 0
test/entt/signal/delegate.cpp

@@ -277,3 +277,18 @@ TEST(Delegate, VoidVsNonVoidReturnType) {
     ASSERT_TRUE(member);
     ASSERT_TRUE(cmember);
 }
+
+TEST(Delegate, TheLessTheBetter) {
+    delegate_functor functor;
+    entt::delegate<int(int, char)> delegate;
+
+    // int delegate_function(const int &);
+    delegate.connect<&delegate_function>();
+
+    ASSERT_EQ(delegate(3, 'c'), 9);
+
+    // int delegate_functor::operator()(int);
+    delegate.connect<&delegate_functor::operator()>(&functor);
+
+    ASSERT_EQ(delegate(3, 'c'), 6);
+}