#pragma region CPL License /* Nuclex Native Framework Copyright (C) 2002-2023 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #pragma endregion // CPL License #ifndef NUCLEX_SUPPORT_EVENTS_EVENT_H #define NUCLEX_SUPPORT_EVENTS_EVENT_H #include "Nuclex/Support/Config.h" #include "Nuclex/Support/Events/Delegate.h" #include // for std::copy_n() #include // for std::vector #include // for std::uint8_t namespace Nuclex { namespace Support { namespace Events { // ------------------------------------------------------------------------------------------- // // Prototype, required for variable argument template template class Event; // ------------------------------------------------------------------------------------------- // /// Manages a list of subscribers that receive callbacks when the event fires /// Type of results the callbacks will return /// Types of the arguments accepted by the callback /// /// /// This is the signal part of a standard signal/slot implementation. The name has been /// chosen because std::signal already defines the term 'signal' for /// an entirely thing and the term 'event' is the second most common term for this kind /// of system. /// /// /// The design makes a few assumptions on the usage patterns it optimizes for. It assumes /// that events typically have a very small number of subscribers and that events should be /// as lean as possible (i.e. rather than expose a single big multi-purpose notification, /// classes would expose multiple granular events to notify about different things). /// It also assumes that firing will happen much more often than subscribing/unsubscribing, /// and subscribing is given slightly more performance priority than unsubscribing. /// /// /// This variant of the event class is not thread safe. The order in which subscribers /// are notified is not defined and may change even between individual calls. Subscribers /// are allowed to unsubscribe themselves during an event call, but not others. Adding /// new event subscriptions from within a call is supported, too. /// /// /// An event should be equivalent in size to 5 pointers (depending on the /// value of the constant)) /// /// /// Usage example: /// /// /// /// int Dummy(int first, std::string second) { return 123; } /// /// class Mock { /// public: int Dummy(int first, std::string second) { return 456; } /// }; /// /// int main() { /// typedef Event<int(int foo, std::string bar)> FooBarEvent; /// /// FooBarEvent test; /// /// // Subscribe the dummy function /// test.Subscribe<Dummy>(); /// /// // Subscribe an object method /// Mock myMock; /// test.Subscribe<Mock, &Mock::Dummy>(&myMock); /// /// // Fire the event /// std::vector<int> returnedValues = test(123, u8"Hello"); /// /// // Fire the event again but don't collect returned values /// test.Emit(123, u8"Hello"); /// } /// /// /// /// Cheat sheet /// /// /// 🛈 Optimized for granular events (many event instances w/few subscribers)
/// 🛈 Optimized for fast broadcast performance over subscribe/unsubscribe
/// 🛈 No allocations up to subscribers
/// âš« Can optionally collect return values from all event callbacks
/// âš« New subscribers can be added freely even during event broadcast
/// âš« Subscribers can unsubscribe themselves even from within event callback
/// 🛇 UNDEFINED BEHAVIOR on unsubscribing any other than self from within callback
/// âš« For single-threaded use (publishers and subscribers share a single thread)
/// 🛇 UNDEFINED BEHAVIOR when accessed from multiple threads
/// -> Multi-threaded broadcast is okay if no subscribe/unsubscribe happens /// (i.e. subscribe phase, then threads run, threads end, then unsubscribe phase)
/// 🛇 Lambda expressions can not be subscribers
/// (adds huge runtime costs, see std::function, would have no way to unsubscribe)
///
/// /// If these restrictions are too much, consider , in which /// basically anything goes for a small price in performance. /// ///
template class Event { /// Number of subscribers the event can handle without allocating memory /// /// To reduce complexity, this value is baked in and not a template argument. It is /// the number of subscriber slots that are baked into the event, enabling it to handle /// a small number of subscribers without allocating heap memory. Each slot takes the size /// of a delegate, 8 bytes on a 32 bit system or 16 bytes on a 64 bit system. If more /// subscribers enlist, the event is forced to allocate memory on the heap. /// private: const static std::size_t BuiltInSubscriberCount = 2; /// Type of value that will be returned by the delegate public: typedef TResult ResultType; /// Method signature for the callbacks notified through this event public: typedef TResult CallType(TArguments...); /// Type of delegate used to call the event's subscribers public: typedef Delegate DelegateType; /// List of results returned by subscribers /// /// Having an std::vector<void> anywhere, even in a SFINAE-disabled method, /// will trigger deprecation compiler warnings on Microsoft compilers. /// Consider this type to be an alias for std::vector<TResult> and nothing else. /// private: typedef std::vector< typename std::conditional::value, char, TResult>::type > ResultVectorType; /// Initializes a new event public: Event() : subscriberCount(0) {} /// Frees all memory used by the event public: ~Event() { if(this->subscriberCount > BuiltInSubscriberCount) { delete []this->heapMemory.Buffer; } } // TODO: Implement copy and move constructors + assignment operators /// Returns the current number of subscribers to the event /// The number of current subscribers public: std::size_t CountSubscribers() const { return this->subscriberCount; } /// Calls all subscribers of the event and collects their return values /// Arguments that will be passed to the event /// An list of the values returned by the event subscribers /// /// This overload is enabled if the event signature returns anything other than 'void'. /// The returned value is an std::vector<TResult> in all cases. /// public: template typename std::enable_if< !std::is_void::value, ResultVectorType >::type operator()(TArguments&&... arguments) const { ResultVectorType results; // ResultVectorType is an alias for std::vector results.reserve(this->subscriberCount); EmitAndCollect(std::back_inserter(results), std::forward(arguments)...); return results; } /// Calls all subscribers of the event /// Arguments that will be passed to the event /// /// This overload is enabled if the event signature has the return type 'void' /// public: template typename std::enable_if< std::is_void::value, void >::type operator()(TArguments&&... arguments) const { Emit(std::forward(arguments)...); } /// Calls all subscribers of the event and collects their return values /// /// Output iterator into which the subscribers' return values will be written /// /// Arguments that will be passed to the event public: template void EmitAndCollect(TOutputIterator results, TArguments&&... arguments) const; /// Calls all subscribers of the event and discards their return values /// Arguments that will be passed to the event public: void Emit(TArguments... arguments) const; /// Subscribes the specified free function to the event /// Free function that will be subscribed public: template void Subscribe() { Subscribe(DelegateType::template Create()); } /// Subscribes the specified object method to the event /// Class the object method is a member of /// Object method that will be subscribed to the event /// Instance on which the object method will be called public: template void Subscribe(TClass *instance) { Subscribe(DelegateType::template Create(instance)); } /// Subscribes the specified const object method to the event /// Class the object method is a member of /// Object method that will be subscribed to the event /// Instance on which the object method will be called public: template void Subscribe(const TClass *instance) { Subscribe(DelegateType::template Create(instance)); } /// Subscribes the specified delegate to the event /// Delegate that will be subscribed public: void Subscribe(const DelegateType &delegate) { if(this->subscriberCount < BuiltInSubscriberCount) { reinterpret_cast(this->stackMemory)[ this->subscriberCount ] = delegate; } else { if(this->subscriberCount == BuiltInSubscriberCount) { convertFromStackToHeapAllocation(); } else if(this->subscriberCount >= this->heapMemory.ReservedSubscriberCount) { growHeapAllocatedList(); } reinterpret_cast(this->heapMemory.Buffer)[ this->subscriberCount ] = delegate; } ++this->subscriberCount; } /// Unsubscribes the specified free function from the event /// /// Free function that will be unsubscribed from the event /// /// True if the object method was subscribed and has been unsubscribed public: template bool Unsubscribe() { return Unsubscribe(DelegateType::template Create()); } /// Unsubscribes the specified object method from the event /// Class the object method is a member of /// /// Object method that will be unsubscribes from the event /// /// Instance on which the object method was subscribed /// True if the object method was subscribed and has been unsubscribed public: template bool Unsubscribe(TClass *instance) { return Unsubscribe(DelegateType::template Create(instance)); } /// Unsubscribes the specified object method from the event /// Class the object method is a member of /// /// Object method that will be unsubscribes from the event /// /// Instance on which the object method was subscribed /// True if the object method was subscribed and has been unsubscribed public: template bool Unsubscribe(const TClass *instance) { return Unsubscribe(DelegateType::template Create(instance)); } /// Unsubscribes the specified delegate from the event /// Delegate that will be unsubscribed /// True if the callback was found and unsubscribed, false otherwise public: bool Unsubscribe(const DelegateType &delegate); /// Switches the event from stack-stored subscribers to heap-stored /// /// For internal use only; this must only be called when the subscriber count is /// equal to the built-in stack storage space and must be followed immediately by /// adding another subscriber (because the subscriberCount is the decision variable /// for the event to know whether to assume heap storage or stack storage). /// private: void convertFromStackToHeapAllocation() { const static std::size_t initialCapacity = BuiltInSubscriberCount * 8; std::uint8_t *initialBuffer = new std::uint8_t[ sizeof(DelegateType[2]) * initialCapacity / 2 ]; // CHECK: Do we risk alignment issues here? std::copy_n( this->stackMemory, sizeof(DelegateType[2]) * BuiltInSubscriberCount / 2, initialBuffer ); this->heapMemory.ReservedSubscriberCount = initialCapacity; this->heapMemory.Buffer = initialBuffer; } /// Increases the size of the heap-allocated list of event subscribers private: void growHeapAllocatedList() { std::size_t newCapacity = this->heapMemory.ReservedSubscriberCount * 2; std::uint8_t *newBuffer = new std::uint8_t[ sizeof(DelegateType[2]) * newCapacity / 2 ]; // CHECK: Do we risk alignment issues here? std::copy_n( this->heapMemory.Buffer, sizeof(DelegateType[2]) * this->subscriberCount / 2, newBuffer ); std::swap(this->heapMemory.Buffer, newBuffer); this->heapMemory.ReservedSubscriberCount = newCapacity; delete []newBuffer; } /// Moves the event's subscriber list back into its own stack storage /// /// This must only be called if the event has just shrunk to the number of subscribers /// that can fit into built-in stack space for storing event subscriptions. The call is /// mandatory (because the subscriberCount is the decision variable for the event to /// know whether to assume heap storage or stack storage). /// private: void convertFromHeapToStackAllocation() { std::uint8_t *oldBuffer = this->heapMemory.Buffer; std::copy_n( oldBuffer, sizeof(DelegateType[2]) * BuiltInSubscriberCount / 2, this->stackMemory ); delete []oldBuffer; } /// Information about subscribers if the list is moved to the heap private: struct HeapAllocatedSubscribers { /// Number of subscribers for which space has been reserved on the heap public: std::size_t ReservedSubscriberCount; /// Dynamically allocated memory the subscribers are stored in public: alignas(DelegateType) std::uint8_t *Buffer; }; /// Number of subscribers that have registered to the event private: std::size_t subscriberCount; /// Stores the first n subscribers inside the event's own memory private: union { HeapAllocatedSubscribers heapMemory; std::uint8_t stackMemory[sizeof(DelegateType[BuiltInSubscriberCount])]; }; }; // ------------------------------------------------------------------------------------------- // template template void Event::EmitAndCollect( TOutputIterator results, TArguments&&... arguments ) const { std::size_t knownSubscriberCount = this->subscriberCount; const DelegateType *subscribers; std::size_t index = 0; // Is the subscriber list currently on the stack? if(knownSubscriberCount <= BuiltInSubscriberCount) { ProcessStackSubscribers: subscribers = reinterpret_cast(this->stackMemory); while(index < knownSubscriberCount) { *results = subscribers[index](std::forward(arguments)...); ++results; if(this->subscriberCount == knownSubscriberCount) { ++index; // Only increment if the current callback wasn't unsubscribed } else if(this->subscriberCount > knownSubscriberCount) { ++index; if(knownSubscriberCount > BuiltInSubscriberCount) { knownSubscriberCount = this->subscriberCount; goto ProcessHeapSubscribers; } knownSubscriberCount = this->subscriberCount; } else { knownSubscriberCount = this->subscriberCount; } } return; } // The subscriber list is currently on the heap { ProcessHeapSubscribers: subscribers = reinterpret_cast(this->heapMemory.Buffer); while(index < knownSubscriberCount) { *results = subscribers[index](std::forward(arguments)...); ++results; if(this->subscriberCount == knownSubscriberCount) { ++index; // Only increment if the current callback wasn't unsubscribed } else if(this->subscriberCount < knownSubscriberCount) { if(knownSubscriberCount <= BuiltInSubscriberCount) { knownSubscriberCount = this->subscriberCount; goto ProcessStackSubscribers; } knownSubscriberCount = this->subscriberCount; } else { ++index; knownSubscriberCount = this->subscriberCount; // In case more heap memory had to be allocated subscribers = reinterpret_cast(this->heapMemory.Buffer); } } return; } } // ------------------------------------------------------------------------------------------- // template void Event::Emit(TArguments... arguments) const { std::size_t knownSubscriberCount = this->subscriberCount; const DelegateType *subscribers; std::size_t index = 0; // Is the subscriber list currently on the stack? if(knownSubscriberCount <= BuiltInSubscriberCount) { ProcessStackSubscribers: subscribers = reinterpret_cast(this->stackMemory); while(index < knownSubscriberCount) { subscribers[index](std::forward(arguments)...); if(this->subscriberCount == knownSubscriberCount) { ++index; // Only increment if the current callback wasn't unsubscribed } else if(this->subscriberCount > knownSubscriberCount) { ++index; if(knownSubscriberCount > BuiltInSubscriberCount) { knownSubscriberCount = this->subscriberCount; goto ProcessHeapSubscribers; } knownSubscriberCount = this->subscriberCount; } else { knownSubscriberCount = this->subscriberCount; } } return; } // The subscriber list is currently on the heap { ProcessHeapSubscribers: subscribers = reinterpret_cast(this->heapMemory.Buffer); while(index < knownSubscriberCount) { subscribers[index](std::forward(arguments)...); if(this->subscriberCount == knownSubscriberCount) { ++index; // Only increment if the current callback wasn't unsubscribed } else if(this->subscriberCount < knownSubscriberCount) { if(knownSubscriberCount <= BuiltInSubscriberCount) { knownSubscriberCount = this->subscriberCount; goto ProcessStackSubscribers; } knownSubscriberCount = this->subscriberCount; } else { ++index; knownSubscriberCount = this->subscriberCount; // In case more heap memory had to be allocated subscribers = reinterpret_cast(this->heapMemory.Buffer); } } return; } } // ------------------------------------------------------------------------------------------- // template bool Event::Unsubscribe(const DelegateType &delegate) { if(this->subscriberCount <= BuiltInSubscriberCount) { DelegateType *subscribers = reinterpret_cast(this->stackMemory); for(std::size_t index = 0; index < this->subscriberCount; ++index) { if(subscribers[index] == delegate) { std::size_t lastSubscriberIndex = this->subscriberCount - 1; subscribers[index] = subscribers[lastSubscriberIndex]; --this->subscriberCount; return true; } } } else { DelegateType *subscribers = reinterpret_cast(this->heapMemory.Buffer); std::size_t lastSubscriberIndex = this->subscriberCount; if(lastSubscriberIndex > 0) { --lastSubscriberIndex; // Tiny optimization. Often the removed event is the last one registered if(subscribers[lastSubscriberIndex] == delegate) { --this->subscriberCount; if(this->subscriberCount <= BuiltInSubscriberCount) { convertFromHeapToStackAllocation(); } return true; } for(std::size_t index = 0; index < lastSubscriberIndex; ++index) { if(subscribers[index] == delegate) { subscribers[index] = subscribers[lastSubscriberIndex]; --this->subscriberCount; if(this->subscriberCount <= BuiltInSubscriberCount) { convertFromHeapToStackAllocation(); } return true; } } } } return false; } // ------------------------------------------------------------------------------------------- // }}} // namespace Nuclex::Support::Events #endif // NUCLEX_SUPPORT_EVENTS_EVENT_H