瀏覽代碼

dispatcher: API review to support named queues

Michele Caini 4 年之前
父節點
當前提交
e7262660c2
共有 6 個文件被更改,包括 139 次插入91 次删除
  1. 1 1
      TODO
  2. 40 24
      docs/md/signal.md
  3. 57 61
      src/entt/signal/dispatcher.hpp
  4. 39 3
      test/entt/signal/dispatcher.cpp
  5. 1 1
      test/lib/dispatcher/lib.cpp
  6. 1 1
      test/lib/dispatcher_plugin/plugin.cpp

+ 1 - 1
TODO

@@ -4,8 +4,8 @@
 * add examples (and credits) from @alanjfs :)
 
 WIP:
+* runtime events (emitter)
 * iterator based try_emplace vs try_insert for perf reasons
-* runtime events (dispatcher/emitter)
 * registry: remove reference to basic_sparse_set<E>
 * dedicated entity storage, in-place O(1) release/destroy for non-orphaned entities, out-of-sync model
 * custom allocators all over

+ 40 - 24
docs/md/signal.md

@@ -11,6 +11,7 @@
   * [Lambda support](#lambda-support)
 * [Signals](#signals)
 * [Event dispatcher](#event-dispatcher)
+  * [Named queues](#named-queues)
 * [Event emitter](#event-emitter)
 <!--
 @endcond TURN_OFF_DOXYGEN
@@ -375,23 +376,20 @@ signal.collect(std::ref(collector));
 
 # Event dispatcher
 
-The event dispatcher class is designed so as to be used in a loop. It allows
-users both to trigger immediate events or to queue events to be published all
-together once per tick.<br/>
-This class shares part of its API with the one of the signal handler, but it
-doesn't require that all the types of events are specified when declared:
+The event dispatcher class allows users to trigger immediate events or to queue
+and publish them all together later.<br/>
+This class lazily instantiates its queues. Therefore, it's not necessary to
+_announce_ the event types in advance:
 
 ```cpp
 // define a general purpose dispatcher
 entt::dispatcher dispatcher{};
 ```
 
-In order to register an instance of a class to a dispatcher, its type must
-expose one or more member functions the arguments of which are such that `E &`
-can be converted to them for each type of event `E`, no matter what the return
-value is.<br/>
-The name of the member function aimed to receive the event must be provided to
-the `connect` member function of the sink in charge for the specific event:
+A listener registered with a dispatcher is such that its type offers one or more
+member functions that take arguments of type `Event &` for any type of event,
+regardless of the return value.<br/>
+These functions are linked directly via `connect` to a _sink_:
 
 ```cpp
 struct an_event { int value; };
@@ -409,8 +407,8 @@ dispatcher.sink<an_event>().connect<&listener::receive>(listener);
 dispatcher.sink<another_event>().connect<&listener::method>(listener);
 ```
 
-The `disconnect` member function follows the same pattern and can be used to
-remove one listener at a time or all of them at once:
+The `disconnect` member function is used to remove one listener at a time or all
+of them at once:
 
 ```cpp
 dispatcher.sink<an_event>().disconnect<&listener::receive>(listener);
@@ -418,14 +416,10 @@ dispatcher.sink<another_event>().disconnect(listener);
 ```
 
 The `trigger` member function serves the purpose of sending an immediate event
-to all the listeners registered so far. It offers a convenient approach that
-relieves users from having to create the event itself. Instead, it's enough to
-specify the type of event and provide all the parameters required to construct
-it.<br/>
-As an example:
+to all the listeners registered so far:
 
 ```cpp
-dispatcher.trigger<an_event>(42);
+dispatcher.trigger(an_event{42});
 dispatcher.trigger<another_event>();
 ```
 
@@ -434,16 +428,14 @@ method can be used to push around urgent messages like an _is terminating_
 notification on a mobile app.
 
 On the other hand, the `enqueue` member function queues messages together and
-allows to maintain control over the moment they are sent to listeners. The
-signature of this method is more or less the same of `trigger`:
+helps to maintain control over the moment they are sent to listeners:
 
 ```cpp
 dispatcher.enqueue<an_event>(42);
-dispatcher.enqueue<another_event>();
+dispatcher.enqueue(another_event{});
 ```
 
-Events are stored aside until the `update` member function is invoked, then all
-the messages that are still pending are sent to the listeners at once:
+Events are stored aside until the `update` member function is invoked:
 
 ```cpp
 // emits all the events of the given type at once
@@ -456,6 +448,30 @@ dispatcher.update();
 This way users can embed the dispatcher in a loop and literally dispatch events
 once per tick to their systems.
 
+## Named queues
+
+All queues within a dispatcher are associated by default with an event type and
+then retrieved from it.<br/>
+However, it's possible to create queues with different _names_ (and therefore
+also multiple queues for a single type). In fact, more or less all functions
+also take an additional parameter. As an example:
+
+```cpp
+dispatcher.sink<an_event>("custom"_hs).connect<&listener::receive>(listener);
+```
+
+In this case, the term _name_ is misused as these are actual numeric identifiers
+of type `id_type`.<br/>
+An exception to this rule is the `enqueue` function. There is no additional
+parameter for it but rather a different function:
+
+```cpp
+dispatcher.enqueue_hint<an_event>("custom"_hs, 42);
+```
+
+This is mainly due to the template argument deduction rules and unfortunately
+there is no real (elegant) way to avoid it.
+
 # Event emitter
 
 A general purpose event emitter thought mainly for those cases where it comes to

+ 57 - 61
src/entt/signal/dispatcher.hpp

@@ -82,8 +82,8 @@ class dispatcher {
     };
 
     template<typename Event>
-    [[nodiscard]] pool_handler<Event> &assure() {
-        if(auto &&ptr = pools[type_hash<Event>::value()]; !ptr) {
+    [[nodiscard]] pool_handler<Event> &assure(const id_type id) {
+        if(auto &&ptr = pools[id]; !ptr) {
             auto *cpool = new pool_handler<Event>{};
             ptr.reset(cpool);
             return *cpool;
@@ -103,7 +103,7 @@ public:
     dispatcher &operator=(dispatcher &&) = default;
 
     /**
-     * @brief Returns a sink object for the given event.
+     * @brief Returns a sink object for the given event and queue.
      *
      * A sink is an opaque object used to connect listeners to events.
      *
@@ -117,69 +117,77 @@ public:
      * @sa sink
      *
      * @tparam Event Type of event of which to get the sink.
+     * @param id Name used to map the event queue within the dispatcher.
      * @return A temporary sink object.
      */
     template<typename Event>
-    [[nodiscard]] auto sink() {
-        return assure<Event>().bucket();
+    [[nodiscard]] auto sink(const id_type id = type_hash<Event>::value()) {
+        return assure<Event>(id).bucket();
     }
 
     /**
-     * @brief Triggers an immediate event of the given type.
-     *
-     * All the listeners registered for the given type are immediately notified.
-     * The event is discarded after the execution.
-     *
+     * @brief Triggers an immediate event of a given type.
      * @tparam Event Type of event to trigger.
-     * @tparam Args Types of arguments to use to construct the event.
-     * @param args Arguments to use to construct the event.
+     * @param event An instance of the given type of event.
      */
-    template<typename Event, typename... Args>
-    void trigger(Args &&...args) {
-        assure<Event>().trigger(Event{std::forward<Args>(args)...});
+    template<typename Event>
+    void trigger(Event &&event = {}) {
+        trigger(type_hash<std::decay_t<Event>>::value(), std::forward<Event>(event));
     }
 
     /**
-     * @brief Triggers an immediate event of the given type.
-     *
-     * All the listeners registered for the given type are immediately notified.
-     * The event is discarded after the execution.
-     *
+     * @brief Triggers an immediate event on a queue of a given type.
      * @tparam Event Type of event to trigger.
      * @param event An instance of the given type of event.
+     * @param id Name used to map the event queue within the dispatcher.
      */
     template<typename Event>
-    void trigger(Event &&event) {
-        assure<std::decay_t<Event>>().trigger(std::forward<Event>(event));
+    void trigger(const id_type id, Event &&event = {}) {
+        assure<std::decay_t<Event>>(id).trigger(std::forward<Event>(event));
     }
 
     /**
      * @brief Enqueues an event of the given type.
-     *
-     * An event of the given type is queued. No listener is invoked. Use the
-     * `update` member function to notify listeners when ready.
-     *
      * @tparam Event Type of event to enqueue.
      * @tparam Args Types of arguments to use to construct the event.
      * @param args Arguments to use to construct the event.
      */
     template<typename Event, typename... Args>
     void enqueue(Args &&...args) {
-        assure<Event>().enqueue(std::forward<Args>(args)...);
+        enqueue_hint<Event>(type_hash<Event>::value(), std::forward<Args>(args)...);
     }
 
     /**
      * @brief Enqueues an event of the given type.
-     *
-     * An event of the given type is queued. No listener is invoked. Use the
-     * `update` member function to notify listeners when ready.
-     *
      * @tparam Event Type of event to enqueue.
      * @param event An instance of the given type of event.
      */
     template<typename Event>
     void enqueue(Event &&event) {
-        assure<std::decay_t<Event>>().enqueue(std::forward<Event>(event));
+        enqueue_hint(type_hash<std::decay_t<Event>>::value(), std::forward<Event>(event));
+    }
+
+    /**
+     * @brief Enqueues an event of the given type.
+     * @tparam Event Type of event to enqueue.
+     * @tparam Args Types of arguments to use to construct the event.
+     * @param id Name used to map the event queue within the dispatcher.
+     * @param args Arguments to use to construct the event.
+     */
+    template<typename Event, typename... Args>
+    void enqueue_hint(const id_type id, Args &&...args) {
+        assure<Event>(id).enqueue(std::forward<Args>(args)...);
+    }
+
+    /**
+     * @brief Enqueues an event of the given type.
+     * @tparam Event Type of event to enqueue.
+     * @param id Name used to map the event queue within the dispatcher.
+     * @param event An instance of the given type of event.
+     */
+    template<typename Event>
+    void enqueue_hint(const id_type id, Event &&event) {
+        assure<std::decay_t<Event>>(id).enqueue(std::forward<Event>(event));
     }
 
     /**
@@ -207,45 +215,33 @@ public:
     }
 
     /**
-     * @brief Discards all the events queued so far.
-     *
-     * If no types are provided, the dispatcher will clear all the existing
-     * pools.
-     *
-     * @tparam Event Type of events to discard.
+     * @brief Discards all the events stored so far in a given queue.
+     * @tparam Event Type of event to discard.
+     * @param id Name used to map the event queue within the dispatcher.
      */
-    template<typename... Event>
+    template<typename Event>
+    void clear(const id_type id = type_hash<Event>::value()) {
+        assure<Event>(id).clear();
+    }
+
+    /*! @brief Discards all the events queued so far. */
     void clear() {
-        if constexpr(sizeof...(Event) == 0) {
-            for(auto &&cpool: pools) {
-                cpool.second->clear();
-            }
-        } else {
-            (assure<Event>().clear(), ...);
+        for(auto &&cpool: pools) {
+            cpool.second->clear();
         }
     }
 
     /**
-     * @brief Delivers all the pending events of the given type.
-     *
-     * This method is blocking and it doesn't return until all the events are
-     * delivered to the registered listeners. It's responsibility of the users
-     * to reduce at a minimum the time spent in the bodies of the listeners.
-     *
-     * @tparam Event Type of events to send.
+     * @brief Delivers all the pending events of a given queue.
+     * @tparam Event Type of event to send.
+     * @param id Name used to map the event queue within the dispatcher.
      */
     template<typename Event>
-    void update() {
-        assure<Event>().publish();
+    void update(const id_type id = type_hash<Event>::value()) {
+        assure<Event>(id).publish();
     }
 
-    /**
-     * @brief Delivers all the pending events.
-     *
-     * This method is blocking and it doesn't return until all the events are
-     * delivered to the registered listeners. It's responsibility of the users
-     * to reduce at a minimum the time spent in the bodies of the listeners.
-     */
+    /*! @brief Delivers all the pending events. */
     void update() const {
         for(auto &&cpool: pools) {
             cpool.second->publish();

+ 39 - 3
test/entt/signal/dispatcher.cpp

@@ -1,5 +1,6 @@
 #include <utility>
 #include <gtest/gtest.h>
+#include <entt/core/hashed_string.hpp>
 #include <entt/core/type_traits.hpp>
 #include <entt/signal/dispatcher.hpp>
 
@@ -31,7 +32,7 @@ TEST(Dispatcher, Functionalities) {
     entt::dispatcher dispatcher;
     receiver receiver;
 
-    dispatcher.trigger<one_more_event>(42);
+    dispatcher.trigger(one_more_event{42});
     dispatcher.enqueue<one_more_event>(42);
     dispatcher.update<one_more_event>();
 
@@ -41,7 +42,7 @@ TEST(Dispatcher, Functionalities) {
 
     ASSERT_EQ(receiver.cnt, 1);
 
-    dispatcher.enqueue<another_event>();
+    dispatcher.enqueue(another_event{});
     dispatcher.update<another_event>();
 
     ASSERT_EQ(receiver.cnt, 1);
@@ -55,7 +56,7 @@ TEST(Dispatcher, Functionalities) {
     dispatcher.clear<an_event>();
     dispatcher.update();
 
-    dispatcher.enqueue<an_event>();
+    dispatcher.enqueue(an_event{});
     dispatcher.clear();
     dispatcher.update();
 
@@ -106,3 +107,38 @@ TEST(Dispatcher, OpaqueDisconnect) {
 
     ASSERT_EQ(receiver.cnt, 1);
 }
+
+TEST(Dispatcher, NamedQueue) {
+    using namespace entt::literals;
+
+    entt::dispatcher dispatcher;
+    receiver receiver;
+
+    dispatcher.sink<an_event>("named"_hs).connect<&receiver::receive>(receiver);
+    dispatcher.trigger<an_event>();
+
+    ASSERT_EQ(receiver.cnt, 0);
+
+    dispatcher.trigger("named"_hs, an_event{});
+
+    ASSERT_EQ(receiver.cnt, 1);
+
+    dispatcher.enqueue<an_event>();
+    dispatcher.enqueue(an_event{});
+    dispatcher.enqueue_hint<an_event>("named"_hs);
+    dispatcher.enqueue_hint("named"_hs, an_event{});
+    dispatcher.update<an_event>();
+
+    ASSERT_EQ(receiver.cnt, 1);
+
+    dispatcher.clear<an_event>();
+    dispatcher.update<an_event>("named"_hs);
+
+    ASSERT_EQ(receiver.cnt, 3);
+
+    dispatcher.enqueue_hint<an_event>("named"_hs);
+    dispatcher.clear<an_event>("named"_hs);
+    dispatcher.update<an_event>("named"_hs);
+
+    ASSERT_EQ(receiver.cnt, 3);
+}

+ 1 - 1
test/lib/dispatcher/lib.cpp

@@ -4,5 +4,5 @@
 
 ENTT_API void trigger(entt::dispatcher &dispatcher) {
     dispatcher.trigger<event>();
-    dispatcher.trigger<message>(42);
+    dispatcher.trigger(message{42});
 }

+ 1 - 1
test/lib/dispatcher_plugin/plugin.cpp

@@ -6,7 +6,7 @@ CR_EXPORT int cr_main(cr_plugin *ctx, cr_op operation) {
     switch(operation) {
     case CR_STEP:
         static_cast<entt::dispatcher *>(ctx->userdata)->trigger<event>();
-        static_cast<entt::dispatcher *>(ctx->userdata)->trigger<message>(42);
+        static_cast<entt::dispatcher *>(ctx->userdata)->trigger(message{42});
         break;
     case CR_CLOSE:
     case CR_LOAD: