Просмотр исходного кода

delegate: support for curried functions

Michele Caini 7 лет назад
Родитель
Сommit
24ba692884
5 измененных файлов с 115 добавлено и 48 удалено
  1. 0 2
      TODO
  2. 3 3
      docs/entity.md
  3. 34 0
      docs/signal.md
  4. 58 43
      src/entt/signal/delegate.hpp
  5. 20 0
      test/entt/signal/delegate.cpp

+ 0 - 2
TODO

@@ -7,7 +7,6 @@
 * define basic reactive systems (track entities to which component is attached, track entities from which component is removed, and so on)
 * define systems as composable mixins (initializazion, reactive, update, whatever) with flexible auto-detected arguments (registry, views, etc)
 * runner proposal: https://en.wikipedia.org/wiki/Fork%E2%80%93join_model https://slide-rs.github.io/specs/03_dispatcher.html
-* deep copy of a registry (or use the snapshot stuff to copy components and keep intact ids at least)
 * is it possible to iterate all the components assigned to an entity through a common base class?
 * can we do more for shared libraries? who knows... see #144
 * work stealing job system (see #100)
@@ -23,4 +22,3 @@
 * monostate: make constraint trivially copyable due to atomic optional
 * travis + windows is now available, try it
 * events on replace, so that one can track updated components? indagate impact
-* allow to bind values to delegate on connect with free functions (sbo + check size on costruction to guarantee a zero allocation abstraction)

+ 3 - 3
docs/entity.md

@@ -1034,7 +1034,7 @@ Persistent views are used mainly to iterate multiple components at once:
 auto view = registry.persistent_view<position, velocity>();
 ```
 
-Moreover, filters can be applied to a persistent view to some extents:
+Filtering entities by components is also supported:
 
 ```cpp
 auto view = registry.persistent_view<position, velocity>(entt::type_list<renderable>);
@@ -1043,8 +1043,8 @@ auto view = registry.persistent_view<position, velocity>(entt::type_list<rendera
 In this case, the view will return all the entities that have both components
 `position` and `velocity` but don't have component `renderable`.<br/>
 Exclusive filters (ie the entities that have either `position` or `velocity`)
-aren't supported for performance reasons. Similarly, a filter cannot be applied
-to a persistent view with an empty template parameters list.
+aren't directly supported for performance reasons. Similarly, a filter cannot be
+applied to a persistent view with an empty template parameters list.
 
 There is no need to store views around for they are extremely cheap to
 construct, even though they can be copied without problems and reused freely. In

+ 34 - 0
docs/signal.md

@@ -8,6 +8,7 @@
 * [Introduction](#introduction)
 * [Signals](#signals)
 * [Delegate](#delegate)
+  * [Currying and free functions](#currying-and-free-functions)
 * [Event dispatcher](#event-dispatcher)
 * [Event emitter](#event-emitter)
 <!--
@@ -213,6 +214,39 @@ 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.
 
+## Currying and free functions
+
+The delegate class comes with a rather obscure feature to be considered as an
+_advanced mode_, that is its limited support for curried free functions.
+
+Consider the following definition:
+
+```cpp
+entt::delegate<void(int)> delegate{};
+```
+
+As shown previously, it accepts functions having type `void(int)`. However, we
+can do something more in this case, because of how the delegate class is
+implemented internally (that is something that goes beyond the purposes of this
+document).<br/>
+In particular, the delegate accepts also functions having type `void(T, int)`,
+as long as `sizeof(T)` is lower than or equal to `sizeof(void *)`. The first
+parameter is stored directly by the delegate class and passed to the connected
+function when needed.
+
+In other terms, this works as well with the above definition:
+
+```cpp
+void g(char c, int i) { /* ... */ }
+
+delegate.connect<&g>('c');
+delegate(42);
+```
+
+In this case, the function `g` is invoked with parameters `'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.
+
 # Event dispatcher
 
 The event dispatcher class is designed so as to be used in a loop. It allows

+ 58 - 43
src/entt/signal/delegate.hpp

@@ -2,6 +2,8 @@
 #define ENTT_SIGNAL_DELEGATE_HPP
 
 
+#include <cassert>
+#include <algorithm>
 #include <functional>
 #include <type_traits>
 #include "../config/config.h"
@@ -65,21 +67,22 @@ class delegate;
 
 
 /**
- * @brief Utility class to send around functions and member functions.
+ * @brief Utility class to use to send around functions and member functions.
  *
  * Unmanaged delegate for function pointers and member functions. Users of this
  * class are in charge of disconnecting instances before deleting them.
  *
  * A delegate can be used as general purpose invoker with no memory overhead for
  * free functions and member functions provided along with an instance on which
- * to invoke them.
+ * to invoke them. It comes also with limited support for curried functions.
  *
  * @tparam Ret Return type of a function type.
  * @tparam Args Types of arguments of a function type.
  */
 template<typename Ret, typename... Args>
 class delegate<Ret(Args...)> final {
-    using proto_fn_type = Ret(const void *, Args...);
+    using storage_type = std::aligned_storage_t<sizeof(void *), alignof(void *)>;
+    using proto_fn_type = Ret(storage_type &, Args...);
 
 public:
     /*! @brief Function type of the delegate. */
@@ -87,11 +90,13 @@ public:
 
     /*! @brief Default constructor. */
     delegate() ENTT_NOEXCEPT
-        : fn{nullptr}, ref{nullptr}
-    {}
+        : storage{}, fn{nullptr}
+    {
+        new (&storage) void *{nullptr};
+    }
 
     /**
-     * @brief Constructs a delegate and binds a free function to it.
+     * @brief Constructs a delegate and connects a free function to it.
      * @tparam Function A valid free function pointer.
      */
     template<auto Function>
@@ -102,12 +107,12 @@ public:
     }
 
     /**
-     * @brief Constructs a delegate and binds a member function to it.
+     * @brief Constructs a delegate and connects a member function to it.
      * @tparam Member Member function to connect to the delegate.
      * @tparam Class Type of class to which the member function belongs.
      * @param instance A valid instance of type pointer to `Class`.
      */
-    template<auto Member, typename Class>
+    template<auto Member, typename Class, typename = std::enable_if_t<std::is_member_function_pointer_v<decltype(Member)>>>
     delegate(connect_arg_t<Member>, Class *instance) ENTT_NOEXCEPT
         : delegate{}
     {
@@ -115,60 +120,73 @@ public:
     }
 
     /**
-     * @brief Binds a free function to a delegate.
+     * @brief Connects a free function to a delegate.
      * @tparam Function A valid free function pointer.
      */
     template<auto Function>
     void connect() ENTT_NOEXCEPT {
         static_assert(std::is_invocable_r_v<Ret, decltype(Function), Args...>);
-        ref = nullptr;
+        new (&storage) void *{nullptr};
 
-        fn = [](const void *, Args... args) -> Ret {
+        fn = [](storage_type &, Args... args) -> Ret {
             return std::invoke(Function, args...);
         };
     }
 
     /**
-     * @brief Connects a member function for a given instance to a delegate.
+     * @brief Connects a member function for a given instance or a curried free
+     * function to a delegate.
      *
-     * The delegate isn't responsible for the connected object. Users must
-     * guarantee that the lifetime of the instance overcomes the one of the
-     * delegate.
+     * When used to connect a member function, the delegate isn't responsible
+     * for the connected object. Users must guarantee that the lifetime of the
+     * instance overcomes the one of the delegate.<br/>
+     * When used to connect a curried free function, the linked value must be at
+     * least copyable and such that its size is lower than or equal to the one
+     * of a `void *`. It means that all primitive types are accepted as well as
+     * pointers. Moreover, the signature of the free function must be such that
+     * the value is the first argument before the ones used to define the
+     * delegate itself.
      *
-     * @tparam Member Member function to connect to the delegate.
-     * @tparam Class Type of class to which the member function belongs.
-     * @param instance A valid instance of type pointer to `Class`.
+     * @tparam Candidate Member function or curried free function to connect to
+     * the delegate.
+     * @tparam Type Type of class to which the member function belongs or type
+     * of value used for currying.
+     * @param value_or_instance A valid pointer to an instance of class type or
+     * the value to use for currying.
      */
-    template<auto Member, typename Class>
-    void connect(Class *instance) ENTT_NOEXCEPT {
-        static_assert(std::is_invocable_r_v<Ret, decltype(Member), Class, Args...>);
-        ref = instance;
-
-        fn = [](const void *instance, Args... args) -> Ret {
-            if constexpr(std::is_const_v<Class>) {
-                return std::invoke(Member, static_cast<Class *>(instance), args...);
-            } else {
-                return std::invoke(Member, static_cast<Class *>(const_cast<void *>(instance)), args...);
-            }
+    template<auto Candidate, typename Type>
+    void connect(Type value_or_instance) ENTT_NOEXCEPT {
+        static_assert(sizeof(Type) <= sizeof(void *));
+        static_assert(std::is_invocable_r_v<Ret, decltype(Candidate), Type, Args...>);
+        new (&storage) Type{value_or_instance};
+
+        fn = [](storage_type &storage, Args... args) -> Ret {
+            Type value_or_instance = *reinterpret_cast<Type *>(&storage);
+            return std::invoke(Candidate, value_or_instance, args...);
         };
     }
 
     /**
      * @brief Resets a delegate.
      *
-     * After a reset, a delegate can be safely invoked with no effect.
+     * After a reset, a delegate cannot be invoked anymore.
      */
     void reset() ENTT_NOEXCEPT {
-        ref = nullptr;
+        new (&storage) void *{nullptr};
         fn = nullptr;
     }
 
     /**
-     * @brief Returns the instance bound to a delegate, if any.
-     * @return An opaque pointer to the instance bound to the delegate, if any.
+     * @brief Returns the instance linked to a delegate, if any.
+     *
+     * @warning
+     * Attempting to use an instance returned by a delegate that doesn't contain
+     * a pointer to a member function results in undefined behavior.
+     *
+     * @return An opaque pointer to the instance linked to the delegate, if any.
      */
     const void * instance() const ENTT_NOEXCEPT {
-        return ref;
+        return *reinterpret_cast<const void **>(&storage);
     }
 
     /**
@@ -186,7 +204,8 @@ public:
      * @return The value returned by the underlying function.
      */
     Ret operator()(Args... args) const {
-        return fn(ref, args...);
+        assert(fn);
+        return fn(storage, args...);
     }
 
     /**
@@ -200,27 +219,23 @@ public:
 
     /**
      * @brief Checks if the contents of the two delegates are different.
-     *
-     * Two delegates are identical if they contain the same listener.
-     *
      * @param other Delegate with which to compare.
      * @return True if the two delegates are identical, false otherwise.
      */
     bool operator==(const delegate<Ret(Args...)> &other) const ENTT_NOEXCEPT {
-        return ref == other.ref && fn == other.fn;
+        const char *lhs = reinterpret_cast<const char *>(&storage);
+        const char *rhs = reinterpret_cast<const char *>(&other.storage);
+        return fn == other.fn && std::equal(lhs, lhs + sizeof(storage_type), rhs);
     }
 
 private:
+    mutable storage_type storage;
     proto_fn_type *fn;
-    const void *ref;
 };
 
 
 /**
  * @brief Checks if the contents of the two delegates are different.
- *
- * Two delegates are identical if they contain the same listener.
- *
  * @tparam Ret Return type of a function type.
  * @tparam Args Types of arguments of a function type.
  * @param lhs A valid delegate object.

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

@@ -5,6 +5,10 @@ int delegate_function(const int &i) {
     return i*i;
 }
 
+int curried_function(int i, int j) {
+    return i+j;
+}
+
 struct delegate_functor {
     int operator()(int i) {
         return i+i;
@@ -64,6 +68,7 @@ TEST(Delegate, Comparison) {
     entt::delegate<int(int)> lhs;
     entt::delegate<int(int)> rhs;
     delegate_functor functor;
+    delegate_functor other;
 
     ASSERT_EQ(lhs, entt::delegate<int(int)>{});
     ASSERT_FALSE(lhs != rhs);
@@ -98,6 +103,13 @@ TEST(Delegate, Comparison) {
     ASSERT_TRUE(lhs == rhs);
     ASSERT_EQ(lhs, rhs);
 
+    lhs.connect<&delegate_functor::operator()>(&other);
+
+    ASSERT_EQ(lhs, (entt::delegate<int(int)>{entt::connect_arg<&delegate_functor::operator()>, &other}));
+    ASSERT_TRUE(lhs != rhs);
+    ASSERT_FALSE(lhs == rhs);
+    ASSERT_NE(lhs, rhs);
+
     lhs.reset();
 
     ASSERT_EQ(lhs, (entt::delegate<int(int)>{}));
@@ -181,3 +193,11 @@ TEST(Delegate, ConstInstance) {
     ASSERT_FALSE(delegate);
     ASSERT_EQ(delegate, entt::delegate<int(int)>{});
 }
+
+TEST(Delegate, CurriedFunction) {
+    entt::delegate<int(int)> delegate;
+    delegate.connect<&curried_function>(3);
+
+    ASSERT_TRUE(delegate);
+    ASSERT_EQ(delegate(1), 4);
+}