From 53a4c4be7fcf4ea3f80657dd7a8899230379a241 Mon Sep 17 00:00:00 2001 From: Michele Caini Date: Sat, 28 Oct 2017 00:13:06 +0200 Subject: [PATCH] signalling stuff --- src/entt/signal/bus.hpp | 335 +++++++++++++++++++++++++++++++ src/entt/signal/delegate.hpp | 137 +++++++++++++ src/entt/signal/dispatcher.hpp | 235 ++++++++++++++++++++++ src/entt/signal/emitter.hpp | 344 ++++++++++++++++++++++++++++++++ src/entt/signal/sigh.hpp | 94 +++++---- src/entt/signal/signal.hpp | 272 +++++++++++++++++++++++++ test/entt/signal/bus.cpp | 141 +++++++++++++ test/entt/signal/delegate.cpp | 45 +++++ test/entt/signal/dispatcher.cpp | 47 +++++ test/entt/signal/emitter.cpp | 117 +++++++++++ test/entt/signal/sigh.cpp | 13 ++ test/entt/signal/signal.cpp | 164 +++++++++++++++ 12 files changed, 1896 insertions(+), 48 deletions(-) create mode 100644 src/entt/signal/bus.hpp create mode 100644 src/entt/signal/delegate.hpp create mode 100644 src/entt/signal/dispatcher.hpp create mode 100644 src/entt/signal/emitter.hpp create mode 100644 src/entt/signal/signal.hpp create mode 100644 test/entt/signal/bus.cpp create mode 100644 test/entt/signal/delegate.cpp create mode 100644 test/entt/signal/dispatcher.cpp create mode 100644 test/entt/signal/emitter.cpp create mode 100644 test/entt/signal/signal.cpp diff --git a/src/entt/signal/bus.hpp b/src/entt/signal/bus.hpp new file mode 100644 index 000000000..341edc8bb --- /dev/null +++ b/src/entt/signal/bus.hpp @@ -0,0 +1,335 @@ +#ifndef ENTT_SIGNAL_BUS_HPP +#define ENTT_SIGNAL_BUS_HPP + + +#include +#include +#include "signal.hpp" +#include "sigh.hpp" + + +namespace entt { + + +/** + * @brief Minimal event bus. + * + * Primary template isn't defined on purpose. The main reason for which it + * exists is to work around the doxygen's parsing capabilities. In fact, there + * is no need to declare it actually. + */ +template class, typename...> +class Bus; + + +/** + * @brief Event bus specialization for multiple types. + * + * The event bus is designed to allow an easy registration of specific member + * functions to a bunch of signal handlers (either manager or unmanaged). + * Classes must publicly expose the required member functions to allow the bus + * to detect them for the purpose of registering and unregistering + * instances.
+ * In particular, for each event type `E`, a matching member function has the + * following signature: `void receive(const E &)`. Events will be properly + * redirected to all the listeners by calling the right member functions, if + * any. + * + * @tparam Sig Type of signal handler to use. + * @tparam Event The list of events managed by the bus. + */ +template class Sig, typename Event, typename... Other> +class Bus + : private Bus, private Bus... +{ +public: + /*! @brief Unsigned integer type. */ + using size_type = std::size_t; + + /*! @brief Default constructor, explicit on purpose. */ + explicit Bus() noexcept = default; + /*! @brief Default destructor. */ + ~Bus() noexcept = default; + + /*! @brief Default copy constructor. */ + Bus(const Bus &) = default; + /*! @brief Default move constructor. */ + Bus(Bus &&) = default; + + /*! @brief Default copy assignment operator. @return This bus. */ + Bus & operator=(const Bus &) = default; + /*! @brief Default move assignment operator. @return This bus. */ + Bus & operator=(Bus &&) = default; + + /** + * @brief Unregisters all the member functions of an instance. + * + * A bus is used to convey a certain set of events. This method detects + * and unregisters from the bus all the matching member functions of an + * instance.
+ * For each event type `E`, a matching member function has the following + * signature: `void receive(const E &)`. + * + * @tparam Instance Type of instance to unregister. + * @param instance A valid instance of the right type. + */ + template + void unreg(Instance instance) { + using accumulator_type = int[]; + accumulator_type accumulator = { + (Bus::unreg(instance), 0), + (Bus::unreg(instance), 0)... + }; + return void(accumulator); + } + + /** + * @brief Registers all the member functions of an instance. + * + * A bus is used to convey a certain set of events. This method detects + * and registers to the bus all the matching member functions of an + * instance.
+ * For each event type `E`, a matching member function has the following + * signature: `void receive(const E &)`. + * + * @tparam Instance Type of instance to register. + * @param instance A valid instance of the right type. + */ + template + void reg(Instance instance) { + using accumulator_type = int[]; + accumulator_type accumulator = { + (Bus::reg(instance), 0), + (Bus::reg(instance), 0)... + }; + return void(accumulator); + } + + /** + * @brief Number of listeners connected to the bus. + * @return Number of listeners currently connected. + */ + size_type size() const noexcept { + using accumulator_type = std::size_t[]; + std::size_t sz = Bus::size(); + accumulator_type accumulator = { sz, (sz += Bus::size())... }; + return void(accumulator), sz; + } + + /** + * @brief Returns false is at least a listener is connected to the bus. + * @return True if the bus has no listeners connected, false otherwise. + */ + bool empty() const noexcept { + using accumulator_type = bool[]; + bool ret = Bus::empty(); + accumulator_type accumulator = { ret, (ret = ret && Bus::empty())... }; + return void(accumulator), ret; + } + + /** + * @brief Connects a free function to the bus. + * @tparam Type Type of event to which to connect the function. + * @tparam Function A valid free function pointer. + */ + template + void connect() { + Bus::template connect(); + } + + /** + * @brief Disconnects a free function from the bus. + * @tparam Type Type of event from which to disconnect the function. + * @tparam Function A valid free function pointer. + */ + template + void disconnect() { + Bus::template disconnect(); + } + + /** + * @brief Publishes an event. + * + * All the listeners are notified. Order isn't guaranteed. + * + * @tparam Type Type of event to publish. + * @tparam Args Types of arguments to use to construct the event. + * @param args Arguments to use to construct the event. + */ + template + void publish(Args&&... args) { + Bus::publish(std::forward(args)...); + } +}; + + +/** + * @brief Event bus specialization for a single type. + * + * The event bus is designed to allow an easy registration of a specific member + * function to a signal handler (either manager or unmanaged). + * Classes must publicly expose the required member function to allow the bus to + * detect it for the purpose of registering and unregistering instances.
+ * In particular, a matching member function has the following signature: + * `void receive(const Event &)`. Events of the given type will be properly + * redirected to all the listeners by calling the right member function, if any. + * + * @tparam Sig Type of signal handler to use. + * @tparam Event Type of event managed by the bus. + */ +template class Sig, typename Event> +class Bus { + using signal_type = Sig; + + template + using instance_type = typename signal_type::template instance_type; + + template + auto disconnect(int, instance_type instance) + -> decltype(std::declval().receive(std::declval()), void()) { + signal.template disconnect(std::move(instance)); + } + + template + auto connect(int, instance_type instance) + -> decltype(std::declval().receive(std::declval()), void()) { + signal.template connect(std::move(instance)); + } + + template void disconnect(char, instance_type) {} + template void connect(char, instance_type) {} + +public: + /*! @brief Unsigned integer type. */ + using size_type = typename signal_type::size_type; + + /*! @brief Default constructor, explicit on purpose. */ + explicit Bus() noexcept = default; + /*! @brief Default destructor. */ + virtual ~Bus() noexcept = default; + + /*! @brief Default copy constructor. */ + Bus(const Bus &) = default; + /*! @brief Default move constructor. */ + Bus(Bus &&) = default; + + /*! @brief Default copy assignment operator. @return This bus. */ + Bus & operator=(const Bus &) = default; + /*! @brief Default move assignment operator. @return This bus. */ + Bus & operator=(Bus &&) = default; + + /** + * @brief Unregisters member functions of instances. + * + * This method tries to detect and unregister from the bus matching member + * functions of instances.
+ * A matching member function has the following signature: + * `void receive(const Event &)`. + * + * @tparam Class Type of instance to unregister. + * @param instance A valid instance of the right type. + */ + template + void unreg(instance_type instance) { + disconnect(0, std::move(instance)); + } + + /** + * @brief Tries to register an instance. + * + * This method tries to detect and register to the bus matching member + * functions of instances.
+ * A matching member function has the following signature: + * `void receive(const Event &)`. + * + * @tparam Class Type of instance to register. + * @param instance A valid instance of the right type. + */ + template + void reg(instance_type instance) { + connect(0, std::move(instance)); + } + + /** + * @brief Number of listeners connected to the bus. + * @return Number of listeners currently connected. + */ + size_type size() const noexcept { + return signal.size(); + } + + /** + * @brief Returns false is at least a listener is connected to the bus. + * @return True if the bus has no listeners connected, false otherwise. + */ + bool empty() const noexcept { + return signal.empty(); + } + + /** + * @brief Connects a free function to the bus. + * @tparam Function A valid free function pointer. + */ + template + void connect() { + signal.template connect(); + } + + /** + * @brief Disconnects a free function from the bus. + * @tparam Function A valid free function pointer. + */ + template + void disconnect() { + signal.template disconnect(); + } + + /** + * @brief Publishes an event. + * + * All the listeners are notified. Order isn't guaranteed. + * + * @tparam Args Types of arguments to use to construct the event. + * @param args Arguments to use to construct the event. + */ + template + void publish(Args&&... args) { + signal.publish({ std::forward(args)... }); + } + +private: + signal_type signal; +}; + + +/** + * @brief Managed event bus. + * + * A managed event bus uses the Signal class template as an underlying type. The + * type of the instances is the one required by the signal handler: + * `std::shared_ptr` (a shared pointer). + * + * @tparam Event The list of events managed by the bus. + */ +template +using ManagedBus = Bus; + +/** + * @brief Unmanaged event bus. + * + * An unmanaged event bus uses the SigH class template as an underlying type. + * The type of the instances is the one required by the signal handler: + * `Class *` (a naked pointer).
+ * When it comes to work with this kind of bus, users must guarantee that the + * lifetimes of the instances overcome the one of the bus itself. + * + * @tparam Event The list of events managed by the bus. + */ +template +using UnmanagedBus = Bus; + + +} + + +#endif // ENTT_SIGNAL_BUS_HPP diff --git a/src/entt/signal/delegate.hpp b/src/entt/signal/delegate.hpp new file mode 100644 index 000000000..fb6ef3f55 --- /dev/null +++ b/src/entt/signal/delegate.hpp @@ -0,0 +1,137 @@ +#ifndef ENTT_SIGNAL_DELEGATE_HPP +#define ENTT_SIGNAL_DELEGATE_HPP + + +#include + + +namespace entt { + + +/** + * @brief Basic delegate implementation. + * + * Primary template isn't defined on purpose. All the specializations give a + * compile-time error unless the template parameter is a function type. + */ +template +class Delegate; + + +/** + * @brief A delegate class 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. + * + * @tparam Ret Return type of a function type. + * @tparam Args Types of arguments of a function type. + */ +template +class Delegate final { + using proto_type = Ret(*)(void *, Args...); + using stub_type = std::pair; + + static Ret fallback(void *, Args...) noexcept { return {}; } + + template + static Ret proto(void *, Args... args) { + return (Function)(args...); + } + + template + static Ret proto(void *instance, Args... args) { + return (static_cast(instance)->*Member)(args...); + } + +public: + /*! @brief Default constructor, explicit on purpose. */ + explicit Delegate() noexcept + : stub{std::make_pair(nullptr, &fallback)} + {} + + /** + * @brief Binds a free function to a delegate. + * @tparam Function A valid free function pointer. + */ + template + void connect() noexcept { + stub = std::make_pair(nullptr, &proto); + } + + /** + * @brief Connects a member function for a given instance 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. + * + * @tparam Class Type of class to which the member function belongs. + * @tparam Member Member function to connect to the delegate. + * @param instance A valid instance of type pointer to `Class`. + */ + template + void connect(Class *instance) noexcept { + stub = std::make_pair(instance, &proto); + } + + /** + * @brief Resets a delegate. + * + * After a reset, a delegate can be safely invoked with no effect. + */ + void reset() noexcept { + stub = std::make_pair(nullptr, &fallback); + } + + /** + * @brief Triggers a delegate. + * @param args Arguments to use to invoke the underlying function. + * @return The value returned by the underlying function. + */ + Ret operator()(Args... args) { + return stub.second(stub.first, args...); + } + + /** + * @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 &other) const noexcept { + return stub.first == other.stub.first && stub.second == other.stub.second; + } + +private: + stub_type stub; +}; + + +/** + * @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. + * @param rhs A valid delegate object. + * @return True if the two delegates are different, false otherwise. + */ +template +bool operator!=(const Delegate &lhs, const Delegate &rhs) noexcept { + return !(lhs == rhs); +} + + +} + + +#endif // ENTT_SIGNAL_DELEGATE_HPP diff --git a/src/entt/signal/dispatcher.hpp b/src/entt/signal/dispatcher.hpp new file mode 100644 index 000000000..590738a9f --- /dev/null +++ b/src/entt/signal/dispatcher.hpp @@ -0,0 +1,235 @@ +#ifndef ENTT_SIGNAL_DISPATCHER_HPP +#define ENTT_SIGNAL_DISPATCHER_HPP + + +#include +#include +#include +#include +#include "../core/family.hpp" +#include "signal.hpp" +#include "sigh.hpp" + + +namespace entt { + + +/** + * @brief Basic dispatcher implementation. + * + * A dispatcher can be used either to trigger an immediate event or to enqueue + * events to be published all together once per tick.
+ * Listeners are provided in the form of member functions. For each event of + * type `Event`, listeners must have the following signature: + * `void(const Event &)`. Member functions named `receive` are automatically + * detected and registered or unregistered by the dispatcher. + * + * @tparam Sig Type of the signal handler to use. + */ +template class Sig> +class Dispatcher final { + using event_family = Family; + + template + using instance_type = typename Sig::template instance_type; + + struct BaseSignalWrapper { + virtual ~BaseSignalWrapper() = default; + virtual void publish(std::size_t) = 0; + }; + + template + struct SignalWrapper final: BaseSignalWrapper { + void publish(std::size_t current) final override { + for(auto &&event: events[current]) { + signal.publish(event); + } + + events[current].clear(); + } + + template + inline void connect(instance_type instance) noexcept { + signal.template connect(std::move(instance)); + } + + template + inline void disconnect(instance_type instance) noexcept { + signal.template disconnect(std::move(instance)); + } + + template + inline void trigger(Args&&... args) { + signal.publish({ std::forward(args)... }); + } + + template + inline void enqueue(std::size_t current, Args&&... args) { + events[current].push_back({ std::forward(args)... }); + } + + private: + Sig signal{}; + std::vector events[2]; + }; + + inline static std::size_t buffer(bool mode) { + return mode ? 0 : 1; + } + + template + SignalWrapper & wrapper() { + auto type = event_family::type(); + + if(!(type < wrappers.size())) { + wrappers.resize(type + 1); + } + + if(!wrappers[type]) { + wrappers[type] = std::make_unique>(); + } + + return static_cast &>(*wrappers[type]); + } + +public: + /*! @brief Default constructor, explicit on purpose. */ + explicit Dispatcher() noexcept + : wrappers{}, mode{false} + {} + + /*! @brief Default destructor. */ + ~Dispatcher() = default; + + /*! @brief Default copy constructor. */ + Dispatcher(const Dispatcher &) = default; + /*! @brief Default move constructor. */ + Dispatcher(Dispatcher &&) = default; + + /*! @brief Default copy assignment operator. @return This dispatcher. */ + Dispatcher & operator=(const Dispatcher &) = default; + /*! @brief Default move assignment operator. @return This dispatcher. */ + Dispatcher & operator=(Dispatcher &&) = default; + + /** + * @brief Registers a listener given in the form of a member function. + * + * A matching member function has the following signature: + * `void receive(const Event &)`. Member functions named `receive` are + * automatically detected and registered if available. + * + * @warning + * Connecting a listener during an update may lead to unexpected behavior. + * Register listeners before or after invoking the update if possible. + * + * @tparam Event Type of event to which to connect the function. + * @tparam Class Type of class to which the member function belongs. + * @tparam Member Member function to connect to the signal. + * @param instance A valid instance of the right type. + */ + template + void connect(instance_type instance) noexcept { + wrapper().template connect(std::move(instance)); + } + + /** + * @brief Unregisters a listener given in the form of a member function. + * + * A matching member function has the following signature: + * `void receive(const Event &)`. Member functions named `receive` are + * automatically detected and unregistered if available. + * + * @warning + * Disonnecting a listener during an update may lead to unexpected behavior. + * Unregister listeners before or after invoking the update if possible. + * + * @tparam Event Type of event from which to disconnect the function. + * @tparam Class Type of class to which the member function belongs. + * @tparam Member Member function to connect to the signal. + * @param instance A valid instance of the right type. + */ + template + void disconnect(instance_type instance) noexcept { + wrapper().template disconnect(std::move(instance)); + } + + /** + * @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. + * + * @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. + */ + template + void trigger(Args&&... args) { + wrapper().trigger(std::forward(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 trigger. + * @tparam Args Types of arguments to use to construct the event. + * @param args Arguments to use to construct the event. + */ + template + void enqueue(Args&&... args) { + wrapper().enqueue(buffer(mode), std::forward(args)...); + } + + /** + * @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 responsability of the users + * to reduce at a minimum the time spent in the bodies of the listeners. + */ + void update() { + auto buf = buffer(mode); + mode = !mode; + + for(auto &&wrapper: wrappers) { + if(wrapper) { + wrapper->publish(buf); + } + } + } + +private: + std::vector> wrappers; + bool mode; +}; + + +/** + * @brief Managed dispatcher. + * + * A managed dispatcher uses the Signal class template as an underlying type. + * The type of the instances is the one required by the signal handler: + * `std::shared_ptr` (a shared pointer). + */ +using ManagedDispatcher = Dispatcher; + + +/** + * @brief Unmanaged dispatcher. + * + * An unmanaged dispatcher uses the SigH class template as an underlying type. + * The type of the instances is the one required by the signal handler: + * `Class *` (a naked pointer).
+ * When it comes to work with this kind of dispatcher, users must guarantee that + * the lifetimes of the instances overcome the one of the dispatcher itself. + */ +using UnmanagedDispatcher = Dispatcher; + + +} + + +#endif // ENTT_SIGNAL_DISPATCHER_HPP diff --git a/src/entt/signal/emitter.hpp b/src/entt/signal/emitter.hpp new file mode 100644 index 000000000..665efce82 --- /dev/null +++ b/src/entt/signal/emitter.hpp @@ -0,0 +1,344 @@ +#ifndef ENTT_SIGNAL_EMITTER_HPP +#define ENTT_SIGNAL_EMITTER_HPP + + +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace entt { + + +/** + * @brief General purpose event emitter. + * + * The emitter class template follows the CRTP idiom. To create a custom emitter + * type, derived classes must inherit directly from the base class as: + * + * ```cpp + * struct MyEmitter: Emitter { + * // ... + * } + * ``` + * + * Handlers for the type of events are created internally on the fly. It's not + * required to specify in advance the full list of accepted types.
+ * Moreover, whenever an event is published, an emitter provides the listeners + * with a reference to itself along with a const reference to the event. + * Therefore listeners have an handy way to work with it without incurring in + * the need of capturing a reference to the emitter. + * + * @tparam Derived Actual type of emitter that extends the class template. + */ +template +class Emitter { + struct BaseHandler { + virtual ~BaseHandler() = default; + virtual bool empty() const noexcept = 0; + virtual void clear() noexcept = 0; + }; + + template + struct Handler final: BaseHandler { + using listener_type = std::function; + using element_type = std::pair; + using container_type = std::list; + using connection_type = typename container_type::iterator; + + bool empty() const noexcept override { + auto pred = [](auto &&element){ return element.first; }; + + return std::all_of(onceL.cbegin(), onceL.cend(), pred) && + std::all_of(onL.cbegin(), onL.cend(), pred); + } + + void clear() noexcept override { + if(publishing) { + auto func = [](auto &&element){ element.first = true; }; + std::for_each(onceL.begin(), onceL.end(), func); + std::for_each(onL.begin(), onL.end(), func); + } else { + onceL.clear(); + onL.clear(); + } + } + + inline connection_type once(listener_type listener) { + return onceL.emplace(onceL.cend(), false, std::move(listener)); + } + + inline connection_type on(listener_type listener) { + return onL.emplace(onL.cend(), false, std::move(listener)); + } + + void erase(connection_type conn) noexcept { + conn->first = true; + + if(!publishing) { + auto pred = [](auto &&element){ return element.first; }; + onceL.remove_if(pred); + onL.remove_if(pred); + } + } + + void publish(const Event &event, Derived &ref) { + container_type currentL; + onceL.swap(currentL); + + auto func = [&event, &ref](auto &&element) { + return element.first ? void() : element.second(event, ref); + }; + + publishing = true; + + std::for_each(onL.rbegin(), onL.rend(), func); + std::for_each(currentL.rbegin(), currentL.rend(), func); + + publishing = false; + + onL.remove_if([](auto &&element){ return element.first; }); + } + + private: + bool publishing{false}; + container_type onceL{}; + container_type onL{}; + }; + + static std::size_t next() noexcept { + static std::size_t counter = 0; + return counter++; + } + + template + static std::size_t type() noexcept { + static std::size_t value = next(); + return value; + } + + template + Handler & handler() noexcept { + std::size_t family = type(); + + if(!(family < handlers.size())) { + handlers.resize(family+1); + } + + if(!handlers[family]) { + handlers[family] = std::make_unique>(); + } + + return static_cast &>(*handlers[family]); + } + +public: + /** @brief Type of listeners accepted for the given type of event. */ + template + using Listener = typename Handler::listener_type; + + /** + * @brief Generic connection type for events. + * + * Type of the connection object returned by the event emitter whenever a + * listener for the given type is registered.
+ * It can be used to break connections still in use. + * + * @tparam Event Type of event for which the connection is created. + */ + template + struct Connection final: private Handler::connection_type { + /** @brief Event emitters are friend classes of connections. */ + friend class Emitter; + + /*! @brief Default constructor, explicit on purpose. */ + explicit Connection() = default; + + /*! @brief Default copy constructor. */ + Connection(const Connection &) = default; + /*! @brief Default move constructor. */ + Connection(Connection &&) = default; + + /*! @brief Default destructor. */ + ~Connection() = default; + + /** + * @brief Creates a connection that wraps its underlying instance. + * @param conn A connection object to wrap. + */ + Connection(typename Handler::connection_type conn) + : Handler::connection_type{std::move(conn)} + {} + + /** + * @brief Default copy assignament operator. + * @return This connection. + */ + Connection & operator=(const Connection &) = default; + + /** + * @brief Default move assignment operator. + * @return This connection. + */ + Connection & operator=(Connection &&) = default; + }; + + /*! @brief Default constructor, explicit on purpose. */ + explicit Emitter() noexcept = default; + + /*! @brief Copying an emitter isn't allowed. */ + Emitter(const Emitter &) = delete; + /*! @brief Default move constructor. */ + Emitter(Emitter &&) = default; + + /*! @brief Default destructor. */ + virtual ~Emitter() noexcept { + static_assert(std::is_base_of, Derived>::value, "!"); + } + + /*! @brief Copying an emitter isn't allowed. @return This emitter. */ + Emitter & operator=(const Emitter &) = delete; + /*! @brief Default move assignament operator. @return This emitter. */ + Emitter & operator=(Emitter &&) = default; + + /** + * @brief Emits the given event. + * + * All the listeners registered for the specific event type are invoked with + * the given event. The event type must either have a proper constructor for + * the arguments provided or be an aggregate type. + * + * @tparam Event Type of event to publish. + * @tparam Args Types of arguments to use to construct the event. + * @param args Parameters to use to initialize the event. + */ + template + void publish(Args&&... args) { + handler().publish({ std::forward(args)... }, *static_cast(this)); + } + + /** + * @brief Registers a long-lived listener with the event emitter. + * + * This method can be used to register a listener designed to be invoked + * more than once for the given event type.
+ * The connection returned by the method can be freely discarded. It's meant + * to be used later to disconnect the listener if required. + * + * The listener is as a callable object that can be moved and the type of + * which is `void(const Event &, Derived &)`. + * + * @note + * Whenever an event is emitted, the emitter provides the listener with a + * reference to the derived class. Listeners don't have to capture those + * instances for later uses. + * + * @tparam Event Type of event to which to connect the listener. + * @param listener The listener to register. + * @return Connection object that can be used to disconnect the listener. + */ + template + Connection on(Listener listener) { + return handler().on(std::move(listener)); + } + + /** + * @brief Registers a short-lived listener with the event emitter. + * + * This method can be used to register a listener designed to be invoked + * only once for the given event type.
+ * The connection returned by the method can be freely discarded. It's meant + * to be used later to disconnect the listener if required. + * + * The listener is as a callable object that can be moved and the type of + * which is `void(const Event &, Derived &)`. + * + * @note + * Whenever an event is emitted, the emitter provides the listener with a + * reference to the derived class. Listeners don't have to capture those + * instances for later uses. + * + * @tparam Event Type of event to which to connect the listener. + * @param listener The listener to register. + * @return Connection object that can be used to disconnect the listener. + */ + template + Connection once(Listener listener) { + return handler().once(std::move(listener)); + } + + /** + * @brief Disconnects a listener from the event emitter. + * + * Do not use twice the same connection to disconnect a listener, it results + * in undefined behavior. Once used, discard the connection object. + * + * @tparam Event Type of event of the connection. + * @param conn A valid connection. + */ + template + void erase(Connection conn) noexcept { + handler().erase(std::move(conn)); + } + + /** + * @brief Disconnects all the listeners for the given event type. + * + * All the connections previously returned for the given event are + * invalidated. Using them results in undefined behaviour. + * + * @tparam Event Type of event to reset. + */ + template + void clear() noexcept { + handler().clear(); + } + + /** + * @brief Disconnects all the listeners. + * + * All the connections previously returned are invalidated. Using them + * results in undefined behaviour. + */ + void clear() noexcept { + std::for_each(handlers.begin(), handlers.end(), + [](auto &&handler){ if(handler) { handler->clear(); } }); + } + + /** + * @brief Checks if there are listeners registered for the specific event. + * @tparam Event Type of event to test. + * @return True if there are no listeners registered, false otherwise. + */ + template + bool empty() const noexcept { + std::size_t family = type(); + + return (!(family < handlers.size()) || + !handlers[family] || + static_cast &>(*handlers[family]).empty()); + } + + /** + * @brief Checks if there are listeners registered with the event emitter. + * @return True if there are no listeners registered, false otherwise. + */ + bool empty() const noexcept { + return std::all_of(handlers.cbegin(), handlers.cend(), + [](auto &&handler){ return !handler || handler->empty(); }); + } + +private: + std::vector> handlers{}; +}; + + +} + + +#endif // ENTT_SIGNAL_EMITTER_HPP diff --git a/src/entt/signal/sigh.hpp b/src/entt/signal/sigh.hpp index 59750ffc8..a5aab8ddd 100644 --- a/src/entt/signal/sigh.hpp +++ b/src/entt/signal/sigh.hpp @@ -71,24 +71,27 @@ using DefaultCollectorType = typename DefaultCollector::collector_type /** - * @brief Signal handler. + * @brief Unmanaged signal handler declaration. * * Primary template isn't defined on purpose. All the specializations give a * compile-time error unless the template parameter is a function type. + * + * @tparam Function A valid function type. + * @tparam Collector Type of collector to use, if any. */ -template> +template> class SigH; /** - * @brief Signal handler. + * @brief Unmanaged signal handler definition. * * Unmanaged signal handler. It works directly with naked pointers to classes * and pointers to member functions as well as pointers to free functions. Users * of this class are in charge of disconnecting instances before deleting them. * * This class serves mainly two purposes: - * * Creating signals to be used later to notify a bunch of listeners. + * * Creating signals used later to notify a bunch of listeners. * * Collecting results from a set of functions like in a voting system. * * The default collector does nothing. To properly collect data, define and use @@ -98,8 +101,8 @@ class SigH; * otherwise. * * @tparam Ret Return type of a function type. - * @tparam Args Types of the arguments of a function type. - * @tparam Collector The type of the collector to use if any. + * @tparam Args Types of arguments of a function type. + * @tparam Collector Type of collector to use, if any. */ template class SigH final: private Invoker { @@ -121,6 +124,13 @@ public: /*! @brief Collector type. */ using collector_type = Collector; + /** + * @brief Instance type when it comes to connecting member functions. + * @tparam Class Type of class to which the member function belongs. + */ + template + using instance_type = Class *; + /*! @brief Default constructor, explicit on purpose. */ explicit SigH() noexcept = default; @@ -129,7 +139,7 @@ public: /** * @brief Copy constructor, listeners are also connected to this signal. - * @param other A signal to be used as source to initialize this instance. + * @param other A signal to use as source to initialize this instance. */ SigH(const SigH &other) : calls{other.calls} @@ -137,15 +147,18 @@ public: /** * @brief Default move constructor. - * @param other A signal to be used as source to initialize this instance. + * @param other A signal to use as source to initialize this instance. */ SigH(SigH &&other): SigH{} { swap(*this, other); } /** - * @brief Assignment operator, listeners are also connected to this signal. - * @param other A signal to be used as source to initialize this instance. + * @brief Copy assignment operator. + * + * Listeners are also connected to this signal. + * + * @param other A signal to use as source to initialize this instance. * @return This signal. */ SigH & operator=(const SigH &other) { @@ -154,8 +167,8 @@ public: } /** - * @brief Default move operator. - * @param other A signal to be used as source to initialize this instance. + * @brief Move assignment operator. + * @param other A signal to use as source to initialize this instance. * @return This signal. */ SigH & operator=(SigH &&other) { @@ -164,15 +177,15 @@ public: } /** - * @brief The number of listeners connected to the signal. - * @return The number of listeners currently connected. + * @brief Number of listeners connected to the signal. + * @return Number of listeners currently connected. */ size_type size() const noexcept { return calls.size(); } /** - * @brief Returns true is at least a listener is connected to the signal. + * @brief Returns false is at least a listener is connected to the signal. * @return True if the signal has no listeners connected, false otherwise. */ bool empty() const noexcept { @@ -180,16 +193,15 @@ public: } /** - * @brief Disconnects all the listeners from the signal. + * @brief Disconnects all the listeners from a signal. */ void clear() noexcept { calls.clear(); } /** - * @brief Connects a free function to the signal. + * @brief Connects a free function to a signal. * - * @note * The signal handler performs checks to avoid multiple connections for free * functions. * @@ -202,28 +214,25 @@ public: } /** - * @brief Connects the member function for the given instance to the signal. + * @brief Connects a member function for a given instance to a signal. * * The signal isn't responsible for the connected object. Users must * guarantee that the lifetime of the instance overcomes the one of the - * signal. + * signal. On the other side, the signal handler performs checks to avoid + * multiple connections for the same member function of a given instance. * - * @warning - * The signal handler performs checks to avoid multiple connections for the - * same member function of a given instance. - * - * @tparam Class The type of the class to which the member function belongs. - * @tparam Member The member function to connect to the signal. + * @tparam Class Type of class to which the member function belongs. + * @tparam Member Member function to connect to the signal. * @param instance A valid instance of type pointer to `Class`. */ template - void connect(Class *instance) { + void connect(instance_type instance) { disconnect(instance); calls.emplace_back(instance, &proto); } /** - * @brief Disconnects a free function from the signal. + * @brief Disconnects a free function from a signal. * @tparam Function A valid free function pointer. */ template @@ -233,30 +242,30 @@ public: } /** - * @brief Disconnects the given member function from the signal. - * @tparam Class The type of the class to which the member function belongs. - * @tparam Member The member function to connect to the signal. + * @brief Disconnects the given member function from a signal. + * @tparam Class Type of class to which the member function belongs. + * @tparam Member Member function to connect to the signal. * @param instance A valid instance of type pointer to `Class`. */ template - void disconnect(Class *instance) { + void disconnect(instance_type instance) { call_type target{instance, &proto}; calls.erase(std::remove(calls.begin(), calls.end(), std::move(target)), calls.end()); } /** * @brief Removes all existing connections for the given instance. - * @tparam Class The type of the class to which the member function belongs. + * @tparam Class Type of class to which the member function belongs. * @param instance A valid instance of type pointer to `Class`. */ template - void disconnect(Class *instance) { + void disconnect(instance_type instance) { auto func = [instance](const call_type &call) { return call.first == instance; }; calls.erase(std::remove_if(calls.begin(), calls.end(), std::move(func)), calls.end()); } /** - * @brief Triggers the signal. + * @brief Triggers a signal. * * All the listeners are notified. Order isn't guaranteed. * @@ -305,7 +314,7 @@ public: * @return True if the two signals are identical, false otherwise. */ bool operator==(const SigH &other) const noexcept { - return (calls.size() == other.calls.size()) && std::equal(calls.cbegin(), calls.cend(), other.calls.cbegin()); + return std::equal(calls.cbegin(), calls.cend(), other.calls.cbegin(), other.calls.cend()); } private: @@ -320,7 +329,7 @@ private: * listeners registered exactly in the same order. * * @tparam Ret Return type of a function type. - * @tparam Args Types of the arguments of a function type. + * @tparam Args Types of arguments of a function type. * @param lhs A valid signal object. * @param rhs A valid signal object. * @return True if the two signals are different, false otherwise. @@ -331,17 +340,6 @@ bool operator!=(const SigH &lhs, const SigH &rhs) no } -/** - * @brief Event handler. - * - * Unmanaged event handler. Collecting data for this kind of signals doesn't - * make sense at all. Its sole purpose is to provide the listeners with the - * given event. - */ -template -using EventH = SigH; - - } diff --git a/src/entt/signal/signal.hpp b/src/entt/signal/signal.hpp new file mode 100644 index 000000000..8650f6c77 --- /dev/null +++ b/src/entt/signal/signal.hpp @@ -0,0 +1,272 @@ +#ifndef ENTT_SIGNAL_SIGNAL_HPP +#define ENTT_SIGNAL_SIGNAL_HPP + + +#include +#include +#include +#include +#include +#include + + +namespace entt { + + +/** + * @brief Managed signal handler declaration. + * + * Primary template isn't defined on purpose. All the specializations give a + * compile-time error unless the template parameter is a function type. + */ +template +class Signal; + + +/** + * @brief Managed signal handler definition. + * + * Managed signal handler. It works with weak pointers to classes and pointers + * to member functions as well as pointers to free functions. References are + * automatically removed when the instances to which they point are freed. + * + * This class can be used to create signals used later to notify a bunch of + * listeners. + * + * @tparam Args Types of arguments of a function type. + */ +template +class Signal final { + using proto_type = bool(*)(std::weak_ptr &, Args...); + using call_type = std::pair, proto_type>; + + template + static bool proto(std::weak_ptr &, Args... args) { + Function(args...); + return true; + } + + template + static bool proto(std::weak_ptr &wptr, Args... args) { + bool ret = false; + + if(!wptr.expired()) { + auto ptr = std::static_pointer_cast(wptr.lock()); + (ptr.get()->*Member)(args...); + ret = true; + } + + return ret; + } + +public: + /*! @brief Unsigned integer type. */ + using size_type = std::size_t; + + /** + * @brief Instance type when it comes to connecting member functions. + * @tparam Class Type of class to which the member function belongs. + */ + template + using instance_type = std::shared_ptr; + + /*! @brief Default constructor, explicit on purpose. */ + explicit Signal() noexcept = default; + + /*! @brief Default destructor. */ + ~Signal() noexcept = default; + + /** + * @brief Copy constructor, listeners are also connected to this signal. + * @param other A signal to use as source to initialize this instance. + */ + Signal(const Signal &other) + : calls{other.calls} + {} + + /** + * @brief Default move constructor. + * @param other A signal to use as source to initialize this instance. + */ + Signal(Signal &&other): Signal{} { + swap(*this, other); + } + + /** + * @brief Copy assignment operator. + * + * Listeners are also connected to this signal. + * + * @param other A signal to use as source to initialize this instance. + * @return This signal. + */ + Signal & operator=(const Signal &other) { + calls = other.calls; + return *this; + } + + /** + * @brief Move assignment operator. + * @param other A signal to use as source to initialize this instance. + * @return This signal. + */ + Signal & operator=(Signal &&other) { + swap(*this, other); + return *this; + } + + /** + * @brief Number of listeners connected to the signal. + * @return Number of listeners currently connected. + */ + size_type size() const noexcept { + return calls.size(); + } + + /** + * @brief Returns false is at least a listener is connected to the signal. + * @return True if the signal has no listeners connected, false otherwise. + */ + bool empty() const noexcept { + return calls.empty(); + } + + /** + * @brief Disconnects all the listeners from a signal. + */ + void clear() noexcept { + calls.clear(); + } + + /** + * @brief Connects a free function to a signal. + * + * The signal handler performs checks to avoid multiple connections for free + * functions. + * + * @tparam Function A valid free function pointer. + */ + template + void connect() { + disconnect(); + calls.emplace_back(std::weak_ptr{}, &proto); + } + + /** + * @brief Connects a member function for a given instance to a signal. + * + * The signal handler performs checks to avoid multiple connections for the + * same member function of a given instance. + * + * @tparam Class Type of class to which the member function belongs. + * @tparam Member Member function to connect to the signal. + * @param instance A valid instance of type pointer to `Class`. + */ + template + void connect(instance_type instance) { + disconnect(instance); + calls.emplace_back(std::move(instance), &proto); + } + + /** + * @brief Disconnects a free function from a signal. + * @tparam Function A valid free function pointer. + */ + template + void disconnect() { + calls.erase(std::remove_if(calls.begin(), calls.end(), + [](const call_type &call) { return call.second == &proto && !call.first.lock(); } + ), calls.end()); + } + + /** + * @brief Disconnects the given member function from a signal. + * @tparam Class Type of class to which the member function belongs. + * @tparam Member Member function to connect to the signal. + * @param instance A valid instance of type pointer to `Class`. + */ + template + void disconnect(instance_type instance) { + calls.erase(std::remove_if(calls.begin(), calls.end(), + [instance{std::move(instance)}](const call_type &call) { return call.second == &proto && call.first.lock() == instance; } + ), calls.end()); + } + + /** + * @brief Removes all existing connections for the given instance. + * @tparam Class Type of class to which the member function belongs. + * @param instance A valid instance of type pointer to `Class`. + */ + template + void disconnect(instance_type instance) { + calls.erase(std::remove_if(calls.begin(), calls.end(), + [instance{std::move(instance)}](const call_type &call) { return call.first.lock() == instance; } + ), calls.end()); + } + + /** + * @brief Triggers a signal. + * + * All the listeners are notified. Order isn't guaranteed. + * + * @param args Arguments to use to invoke listeners. + */ + void publish(Args... args) { + for(auto it = calls.rbegin(), end = calls.rend(); it != end; it++) { + if(!(it->second)(it->first, args...)) { + calls.erase(std::next(it).base()); + } + } + } + + /** + * @brief Swaps listeners between the two signals. + * @param lhs A valid signal object. + * @param rhs A valid signal object. + */ + friend void swap(Signal &lhs, Signal &rhs) { + using std::swap; + swap(lhs.calls, rhs.calls); + } + + /** + * @brief Checks if the contents of the two signals are identical. + * + * Two signals are identical if they have the same size and the same + * listeners registered exactly in the same order. + * + * @param other Signal with which to compare. + * @return True if the two signals are identical, false otherwise. + */ + bool operator==(const Signal &other) const noexcept { + return std::equal(calls.cbegin(), calls.cend(), other.calls.cbegin(), other.calls.cend(), [](const auto &lhs, const auto &rhs) { + return (lhs.second == rhs.second) && (lhs.first.lock() == rhs.first.lock()); + }); + } + +private: + std::vector calls; +}; + + +/** + * @brief Checks if the contents of the two signals are different. + * + * Two signals are identical if they have the same size and the same + * listeners registered exactly in the same order. + * + * @tparam Args Types of arguments of a function type. + * @param lhs A valid signal object. + * @param rhs A valid signal object. + * @return True if the two signals are different, false otherwise. + */ +template +bool operator!=(const Signal &lhs, const Signal &rhs) noexcept { + return !(lhs == rhs); +} + + +} + + +#endif // ENTT_SIGNAL_SIGNAL_HPP diff --git a/test/entt/signal/bus.cpp b/test/entt/signal/bus.cpp new file mode 100644 index 000000000..75e35d894 --- /dev/null +++ b/test/entt/signal/bus.cpp @@ -0,0 +1,141 @@ +#include +#include +#include + +struct EventA +{ + EventA(int x, int y): value{x+y} {} + int value; +}; + +struct EventB {}; +struct EventC {}; + +struct MyListener +{ + void receive(const EventA &) { A++; } + static void listen(const EventB &) { B++; } + void receive(const EventC &) { C++; } + void reset() { A = 0; B = 0; C = 0; } + int A{0}; + static int B; + int C{0}; +}; + +int MyListener::B = 0; + +template +void testRegUnregEmit(Listener listener) { + Bus bus; + + listener->reset(); + bus.template publish(40, 2); + bus.template publish(); + bus.template publish(); + + ASSERT_EQ(bus.size(), (decltype(bus.size()))0); + ASSERT_TRUE(bus.empty()); + ASSERT_EQ(listener->A, 0); + ASSERT_EQ(listener->B, 0); + ASSERT_EQ(listener->C, 0); + + bus.reg(listener); + bus.template connect(); + + listener->reset(); + bus.template publish(40, 2); + bus.template publish(); + bus.template publish(); + + ASSERT_EQ(bus.size(), (decltype(bus.size()))3); + ASSERT_FALSE(bus.empty()); + ASSERT_EQ(listener->A, 1); + ASSERT_EQ(listener->B, 1); + ASSERT_EQ(listener->C, 1); + + bus.unreg(listener); + + listener->reset(); + bus.template publish(40, 2); + bus.template publish(); + bus.template publish(); + + ASSERT_EQ(bus.size(), (decltype(bus.size()))1); + ASSERT_FALSE(bus.empty()); + ASSERT_EQ(listener->A, 0); + ASSERT_EQ(listener->B, 1); + ASSERT_EQ(listener->C, 0); + + bus.template disconnect(); + + listener->reset(); + bus.template publish(40, 2); + bus.template publish(); + bus.template publish(); + + ASSERT_EQ(bus.size(), (decltype(bus.size()))0); + ASSERT_TRUE(bus.empty()); + ASSERT_EQ(listener->A, 0); + ASSERT_EQ(listener->B, 0); + ASSERT_EQ(listener->C, 0); +} + +TEST(ManagedBus, RegUnregEmit) { + using MyManagedBus = entt::ManagedBus; + testRegUnregEmit(std::make_shared()); +} + +TEST(ManagedBus, ExpiredListeners) { + entt::ManagedBus bus; + auto listener = std::make_shared(); + + listener->reset(); + bus.reg(listener); + bus.template publish(40, 2); + bus.template publish(); + + ASSERT_EQ(bus.size(), (decltype(bus.size()))2); + ASSERT_FALSE(bus.empty()); + ASSERT_EQ(listener->A, 1); + ASSERT_EQ(listener->B, 0); + + listener->reset(); + listener = nullptr; + + ASSERT_EQ(bus.size(), (decltype(bus.size()))2); + ASSERT_FALSE(bus.empty()); + + EXPECT_NO_THROW(bus.template publish(40, 2)); + EXPECT_NO_THROW(bus.template publish()); + + ASSERT_EQ(bus.size(), (decltype(bus.size()))0); + ASSERT_TRUE(bus.empty()); +} + +TEST(UnmanagedBus, RegUnregEmit) { + using MyUnmanagedBus = entt::UnmanagedBus; + auto ptr = std::make_unique(); + testRegUnregEmit(ptr.get()); +} + +TEST(UnmanagedBus, ExpiredListeners) { + entt::UnmanagedBus bus; + auto listener = std::make_unique(); + + listener->reset(); + bus.reg(listener.get()); + bus.template publish(40, 2); + bus.template publish(); + + ASSERT_EQ(bus.size(), (decltype(bus.size()))2); + ASSERT_FALSE(bus.empty()); + ASSERT_EQ(listener->A, 1); + ASSERT_EQ(listener->B, 0); + + listener->reset(); + listener = nullptr; + + // dangling pointer inside ... well, unmanaged means unmanaged!! :-) + ASSERT_EQ(bus.size(), (decltype(bus.size()))2); + ASSERT_FALSE(bus.empty()); +} diff --git a/test/entt/signal/delegate.cpp b/test/entt/signal/delegate.cpp new file mode 100644 index 000000000..d507afb80 --- /dev/null +++ b/test/entt/signal/delegate.cpp @@ -0,0 +1,45 @@ +#include +#include + +int f(int i) { + return i*i; +} + +struct S { + int f(int i) { + return i+i; + } +}; + +TEST(Delegate, Functionalities) { + entt::Delegate ffdel; + entt::Delegate mfdel; + S test; + + ASSERT_EQ(ffdel(42), int{}); + ASSERT_EQ(mfdel(42), int{}); + + ffdel.connect<&f>(); + mfdel.connect(&test); + + ASSERT_EQ(ffdel(3), 9); + ASSERT_EQ(mfdel(3), 6); + + ffdel.reset(); + mfdel.reset(); + + ASSERT_EQ(ffdel(42), int{}); + ASSERT_EQ(mfdel(42), int{}); +} + +TEST(Delegate, Comparison) { + entt::Delegate delegate; + entt::Delegate def; + delegate.connect<&f>(); + + ASSERT_EQ(def, entt::Delegate{}); + ASSERT_NE(def, delegate); + + ASSERT_TRUE(def == entt::Delegate{}); + ASSERT_TRUE (def != delegate); +} diff --git a/test/entt/signal/dispatcher.cpp b/test/entt/signal/dispatcher.cpp new file mode 100644 index 000000000..fd511844f --- /dev/null +++ b/test/entt/signal/dispatcher.cpp @@ -0,0 +1,47 @@ +#include +#include +#include + +struct Event {}; + +struct Receiver { + void receive(const Event &) { ++cnt; } + void reset() { cnt = 0; } + std::size_t cnt{0}; +}; + +template +void testDispatcher(Rec receiver) { + Dispatcher dispatcher; + + dispatcher.template connect(receiver); + dispatcher.template trigger(); + dispatcher.template enqueue(); + + ASSERT_EQ(receiver->cnt, static_castcnt)>(1)); + + dispatcher.update(); + dispatcher.update(); + dispatcher.template trigger(); + + ASSERT_EQ(receiver->cnt, static_castcnt)>(3)); + + receiver->reset(); + + dispatcher.template disconnect(receiver); + dispatcher.template trigger(); + dispatcher.template enqueue(); + dispatcher.update(); + dispatcher.template trigger(); + + ASSERT_EQ(receiver->cnt, static_castcnt)>(0)); +} + +TEST(ManagedDispatcher, Basics) { + testDispatcher(std::make_shared()); +} + +TEST(UnmanagedDispatcher, Basics) { + auto ptr = std::make_unique(); + testDispatcher(ptr.get()); +} diff --git a/test/entt/signal/emitter.cpp b/test/entt/signal/emitter.cpp new file mode 100644 index 000000000..0980ca9e3 --- /dev/null +++ b/test/entt/signal/emitter.cpp @@ -0,0 +1,117 @@ +#include +#include + +struct TestEmitter: entt::Emitter {}; + +struct FooEvent { int i; char c; }; +struct BarEvent {}; + +TEST(Emitter, Clear) { + TestEmitter emitter; + + ASSERT_TRUE(emitter.empty()); + + emitter.on([](const auto &, const auto &){}); + + ASSERT_FALSE(emitter.empty()); + ASSERT_FALSE(emitter.empty()); + ASSERT_TRUE(emitter.empty()); + + emitter.clear(); + + ASSERT_FALSE(emitter.empty()); + ASSERT_FALSE(emitter.empty()); + ASSERT_TRUE(emitter.empty()); + + emitter.clear(); + + ASSERT_TRUE(emitter.empty()); + ASSERT_TRUE(emitter.empty()); + ASSERT_TRUE(emitter.empty()); + + emitter.on([](const auto &, const auto &){}); + emitter.on([](const auto &, const auto &){}); + + ASSERT_FALSE(emitter.empty()); + ASSERT_FALSE(emitter.empty()); + ASSERT_FALSE(emitter.empty()); + + emitter.clear(); + + ASSERT_TRUE(emitter.empty()); + ASSERT_TRUE(emitter.empty()); + ASSERT_TRUE(emitter.empty()); +} + +TEST(Emitter, ClearPublishing) { + TestEmitter emitter; + bool invoked = false; + + ASSERT_TRUE(emitter.empty()); + + emitter.on([&invoked](const auto &, auto &em){ + invoked = true; + em.clear(); + }); + + emitter.publish(); + + ASSERT_TRUE(emitter.empty()); + ASSERT_TRUE(invoked); +} + +TEST(Emitter, On) { + TestEmitter emitter; + + emitter.on([](const auto &, const auto &){}); + + ASSERT_FALSE(emitter.empty()); + ASSERT_FALSE(emitter.empty()); + + emitter.publish(0, 'c'); + + ASSERT_FALSE(emitter.empty()); + ASSERT_FALSE(emitter.empty()); +} + +TEST(Emitter, Once) { + TestEmitter emitter; + + emitter.once([](const auto &, const auto &){}); + + ASSERT_FALSE(emitter.empty()); + ASSERT_FALSE(emitter.empty()); + + emitter.publish(); + + ASSERT_TRUE(emitter.empty()); + ASSERT_TRUE(emitter.empty()); +} + +TEST(Emitter, OnceAndErase) { + TestEmitter emitter; + + auto conn = emitter.once([](const auto &, const auto &){}); + + ASSERT_FALSE(emitter.empty()); + ASSERT_FALSE(emitter.empty()); + + emitter.erase(conn); + + ASSERT_TRUE(emitter.empty()); + ASSERT_TRUE(emitter.empty()); +} + +TEST(Emitter, OnAndErase) { + TestEmitter emitter; + + auto conn = emitter.on([](const auto &, const auto &){}); + + ASSERT_FALSE(emitter.empty()); + ASSERT_FALSE(emitter.empty()); + + emitter.erase(conn); + + ASSERT_TRUE(emitter.empty()); + ASSERT_TRUE(emitter.empty()); +} diff --git a/test/entt/signal/sigh.cpp b/test/entt/signal/sigh.cpp index c0e0dbb5b..a99f9df10 100644 --- a/test/entt/signal/sigh.cpp +++ b/test/entt/signal/sigh.cpp @@ -75,6 +75,17 @@ struct S { static void f(int &v) { v = 42; } }; +TEST(SigH, Clear) { + entt::SigH sigh; + sigh.connect<&S::f>(); + + ASSERT_FALSE(sigh.empty()); + + sigh.clear(); + + ASSERT_TRUE(sigh.empty()); +} + TEST(SigH, Functions) { entt::SigH sigh; int v = 0; @@ -93,6 +104,8 @@ TEST(SigH, Functions) { ASSERT_TRUE(sigh.empty()); ASSERT_EQ((entt::SigH::size_type)0, sigh.size()); ASSERT_EQ(0, v); + + sigh.connect<&S::f>(); } TEST(SigH, Members) { diff --git a/test/entt/signal/signal.cpp b/test/entt/signal/signal.cpp new file mode 100644 index 000000000..18a56789a --- /dev/null +++ b/test/entt/signal/signal.cpp @@ -0,0 +1,164 @@ +#include +#include +#include +#include + +struct S { + static void f(const int &j) { i = j; } + void g(const int &j) { i = j; } + void h(const int &) {} + static int i; +}; + +int S::i = 0; + +TEST(Signal, Lifetime) { + using signal = entt::Signal; + + ASSERT_NO_THROW(signal{}); + + signal src{}, other{}; + + ASSERT_NO_THROW(signal{src}); + ASSERT_NO_THROW(signal{std::move(other)}); + ASSERT_NO_THROW(src = other); + ASSERT_NO_THROW(src = std::move(other)); + + ASSERT_NO_THROW(delete new signal{}); +} + +TEST(Signal, Comparison) { + struct S { + void f() {} + void g() {} + }; + + entt::Signal sig1; + entt::Signal sig2; + + auto s1 = std::make_shared(); + auto s2 = std::make_shared(); + + sig1.connect(s1); + sig2.connect(s2); + + ASSERT_FALSE(sig1 == sig2); + ASSERT_TRUE(sig1 != sig2); + + sig1.disconnect(s1); + sig2.disconnect(s2); + + sig1.connect(s1); + sig2.connect(s1); + + ASSERT_FALSE(sig1 == sig2); + ASSERT_TRUE(sig1 != sig2); + + sig1.disconnect(s1); + sig2.disconnect(s1); + + ASSERT_TRUE(sig1 == sig2); + ASSERT_FALSE(sig1 != sig2); + + sig1.connect(s1); + sig1.connect(s1); + sig2.connect(s1); + sig2.connect(s1); + + ASSERT_TRUE(sig1 == sig2); + + sig1.disconnect(s1); + sig1.disconnect(s1); + sig2.disconnect(s1); + sig2.disconnect(s1); + + sig1.connect(s1); + sig1.connect(s1); + sig2.connect(s1); + sig2.connect(s1); + + ASSERT_FALSE(sig1 == sig2); +} + +TEST(Signal, Clear) { + entt::Signal signal; + signal.connect<&S::f>(); + + ASSERT_FALSE(signal.empty()); + + signal.clear(); + + ASSERT_TRUE(signal.empty()); +} + +TEST(Signal, Functions) { + entt::Signal signal; + auto val = S::i + 1; + + signal.connect<&S::f>(); + signal.publish(val); + + ASSERT_FALSE(signal.empty()); + ASSERT_EQ(entt::Signal::size_type{1}, signal.size()); + ASSERT_EQ(S::i, val); + + signal.disconnect<&S::f>(); + signal.publish(val+1); + + ASSERT_TRUE(signal.empty()); + ASSERT_EQ(entt::Signal::size_type{0}, signal.size()); + ASSERT_EQ(S::i, val); +} + +TEST(Signal, Members) { + entt::Signal signal; + auto ptr = std::make_shared(); + auto val = S::i + 1; + + signal.connect(ptr); + signal.publish(val); + + ASSERT_FALSE(signal.empty()); + ASSERT_EQ(entt::Signal::size_type{1}, signal.size()); + ASSERT_EQ(S::i, val); + + signal.disconnect(ptr); + signal.publish(val+1); + + ASSERT_TRUE(signal.empty()); + ASSERT_EQ(entt::Signal::size_type{0}, signal.size()); + ASSERT_EQ(S::i, val); + + ++val; + + signal.connect(ptr); + signal.connect(ptr); + signal.publish(val); + + ASSERT_FALSE(signal.empty()); + ASSERT_EQ(entt::Signal::size_type{2}, signal.size()); + ASSERT_EQ(S::i, val); + + signal.disconnect(ptr); + signal.publish(val+1); + + ASSERT_TRUE(signal.empty()); + ASSERT_EQ(entt::Signal::size_type{0}, signal.size()); + ASSERT_EQ(S::i, val); +} + +TEST(Signal, Cleanup) { + entt::Signal signal; + auto ptr = std::make_shared(); + signal.connect(ptr); + auto val = S::i; + ptr = nullptr; + + ASSERT_FALSE(signal.empty()); + ASSERT_EQ(S::i, val); + + signal.publish(val); + + ASSERT_TRUE(signal.empty()); + ASSERT_EQ(S::i, val); +}