#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_DELEGATE_H #define NUCLEX_SUPPORT_EVENTS_DELEGATE_H #include "Nuclex/Support/Config.h" #include "Nuclex/Support/Errors/EmptyDelegateCallError.h" #include // for assert() namespace Nuclex { namespace Support { namespace Events { // ------------------------------------------------------------------------------------------- // // Required to declare the type template class Delegate; // ------------------------------------------------------------------------------------------- // /// Callback to a free function, method or lambda expression /// Type that will be returned from the method /// Types of the arguments accepted by the callback /// /// /// A delegate is in principle a glorified function pointer, one that can invoke plain /// free functions as well as object methods (capturing the 'this' pointer of the instance /// the method is to be called on) or lamba expressions. /// /// /// If you're up-to-speed in modern C++, it's essentially like std::function with /// a little bit of std::bind mixed in for the 'this' pointer capture. But unlike /// std::function, it is identity-comparable (i.e. you can check if two delegates are /// invoking the exact same free function, object method or lambda expression). /// /// /// This makes delegates useful to implement subscriptions in publisher/subscriber /// systems (aka. signals/slots) that can be unregistered without magic handles. /// /// /// A delegate should be equivalent in size to two pointers. /// /// /// Usage example: /// /// /// /// void Dummy(int first, std::string second) {} /// /// class Mock { /// public: void Dummy(int first, std::string second) {} /// }; /// /// int main() { /// typedef Delegate<void(int foo, std::string bar)> FooBarDelegate; /// /// // Create a delegate /// FooBarDelegate test = FooBarDelegate::Create<Dummy>(); /// /// // Call the function the delegate is set to /// test(123, u8"Hello"); /// /// // Point the delegate to an object method (could use Reset() here, too) /// Mock myMock; /// test = FooBarDelegate::Create<Mock, &Mock::Dummy>(&myMock); /// /// // Call the object method the delegate is set to /// test(123, u8"Hello"); /// } /// /// /// template class Delegate { /// 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...); /// Creates a delegate that will invoke the specified free function /// Free function that will be called by the delegate /// A delegate that invokes the specified free public: template static Delegate Create() { return Delegate(&Delegate::callFreeFunction); } /// Creates a delegate that will invoke the specified object method /// Class the object method is a member of /// Free function that will be called by the delegate /// Instance on which the object method will be called /// A delegate that invokes the specified free public: template static Delegate Create(TClass *instance) { return Delegate( reinterpret_cast(instance), &Delegate::callObjectMethod ); } /// Creates a delegate that will invoke the specified const object method /// Class the object method is a member of /// Free function that will be called by the delegate /// Instance on which the object method will be called /// A delegate that invokes the specified free public: template static Delegate Create(const TClass *instance) { // Note: This const cast is fine. Casting away const is allowed if you do not // ever modify the object that way. We're only casting it away for storage, // the callConstObjectMethod() call wrapper will cast it on again before calling. return Delegate( const_cast(reinterpret_cast(instance)), &Delegate::callConstObjectMethod ); } /// Initializes a new delegate as copy of an existing delegate /// Existing delegate that will be copied public: Delegate(const Delegate &other) = default; /// Constructs an empty delegate public: Delegate() : #if !defined(NDEBUG) instance(nullptr), #endif method(&Delegate::callEmptyDelegate) {} /// Initializes a new delegate by taking over an existing delegate /// Existing delegate that will be taken over #if !defined(NDEBUG) public: Delegate(Delegate &&other) : instance(other.instance), method(other.method) { other.instance = nullptr; other.method = &Delegate::errorDelegateDestroyed; } #else public: Delegate(Delegate &&other) = default; #endif /// Frees all resources owned by the delegate #if !defined(NDEBUG) public: ~Delegate() { this->instance = nullptr; this->method = &Delegate::errorDelegateDestroyed; } #else public: ~Delegate() = default; #endif /// Invokes the delegate /// Arguments as defined by the call signature /// The value returned by the called delegate, if any public: TResult operator()(TArguments... arguments) const { return (this->*method)(std::forward(arguments)...); } /// Resets the delegate to an empty value public: void Reset() { #if !defined(NDEBUG) this->method = nullptr; #endif this->method = &Delegate::callEmptyDelegate; } /// Resets the delegate to the specified free function /// Free function that will be called by the delegate public: template void Reset() { this->instance = nullptr; this->method = &Delegate::callFreeFunction; } /// Resets the delegate to the specified object method /// Class the object method is a member of /// Object method that will be called by the delegate /// Instance on which the object method will be called public: template void Reset(TClass *instance) { this->instance = reinterpret_cast(instance); this->method = &Delegate::callObjectMethod; } /// Resets the delegate to the specified const object method /// Class the object method is a member of /// Object method that will be called by the delegate /// Instance on which the object method will be called public: template void Reset(const TClass *instance) { // Note: This const cast is fine. Casting away const is allowed if you do not // ever modify the object that way. We're only casting it away for storage, // the callConstObjectMethod() call wrapper will cast it on again before calling. this->instance = const_cast(reinterpret_cast(instance)); this->method = &Delegate::callConstObjectMethod; } /// Makes this delegate a copy of another delegate /// Other delegate that will be copied /// This delegate public: Delegate &operator =(const Delegate &other) = default; /// Lets this delegate take over another delegate /// Other delegate that will be taken over /// This delegate #if !defined(NDEBUG) public: Delegate &operator =(Delegate &&other) { this->instance = other.instance; this->method = other.method; other.instance = nullptr; other.method = &Delegate::errorDelegateDestroyed; } #else public: Delegate &operator =(Delegate &&other) = default; #endif /// Checks whether another delegate is invoking the same target /// Other delegate that will be compared against this one /// True if the other delegate is invoking the same target public: bool operator ==(const Delegate &other) const { if(this->method == &Delegate::callEmptyDelegate) { return (other.method == &Delegate::callEmptyDelegate); } else if(other.method == &Delegate::callEmptyDelegate) { return false; // To avoid comparing uninitialized vars (even if of no consequence) } else { return ( (this->instance == other.instance) && (this->method == other.method) ); } } /// Checks whether another delegate is invoking a different target /// Other delegate that will be compared against this one /// True if the other delegate is invoking a different target public: bool operator !=(const Delegate &other) const { if(this->method == &Delegate::callEmptyDelegate) { return (other.method != &Delegate::callEmptyDelegate); } else if(other.method == &Delegate::callEmptyDelegate) { return true; // To avoid comparing uninitialized vars (even if of no consequence) } else { return ( (this->instance != other.instance) || (this->method != other.method) ); } } /// Checks whether the delegate has a target to invoke /// True if the delegate currently has a target public: bool HasTarget() const { return (this->method != &Delegate::callEmptyDelegate); } /// Checks whether the delegate is unassigned /// True if the delegate is unassigned public: bool operator !() const { return (this->method == &Delegate::callEmptyDelegate); } /// Type of the call wrappers that invoke the target method private: typedef TResult (Delegate::*CallWrapperType)(TArguments...) const; /// Special constructor for internal use by the named constructor methods /// Address that will be assigned to the instance field /// Method used to call the delegate's target private: Delegate(void *instance, CallWrapperType callWrapperMethod) : instance(instance), method(callWrapperMethod) {} /// Special constructor for internal use by the named constructor methods /// Method used to call the delegate's target private: Delegate(CallWrapperType callWrapperMethod) : instance(nullptr), method(callWrapperMethod) {} /// Call wrapper that throws an exception if an empty delegate is called /// The result of the called method or function private: TResult callEmptyDelegate(TArguments...) const { // Since we don't know the return type and there's no guarantee that we can // default-construct one out of thin air (or that that would be the right course // of action), we cannot 'return' and our only choice is to throw. static const std::string message(u8"No call target has been assigned to the delegate"); throw Errors::EmptyDelegateCallError(message); } /// Call wrapper that invokes a free function /// Function that will be invoked private: template TResult callFreeFunction(TArguments... arguments) const { return (TFreeFunction)(std::forward(arguments)...); } /// Call wrapper that invokes a free function /// Class the object method is a member of /// Function that will be invoked private: template TResult callObjectMethod(TArguments... arguments) const { TClass *typedInstance = reinterpret_cast(this->instance); return (typedInstance->*TObjectMethod)(std::forward(arguments)...); } /// Call wrapper that invokes a free function /// Class the object method is a member of /// Function that will be invoked private: template TResult callConstObjectMethod(TArguments... arguments) const { const TClass *typedInstance = reinterpret_cast( const_cast(this->instance) ); return (typedInstance->*TObjectMethod)(std::forward(arguments)...); } #if !defined(NDEBUG) /// Call wrapper that reports when the delegate is called after /// Function that will be invoked /// The result of the called method or function private: TResult errorDelegateDestroyed(TArguments...) const { using namespace std; assert(!u8"Call to destroyed delegate (post-destructor or move operator)"); throw std::logic_error(u8"Call to destroyed delegate (post-destructor or move operator)"); } #endif /// Instance on which the callback will take place, if applicable private: void *instance; /// Address of the call wrapper that will call the subscribed method private: CallWrapperType method; }; // ------------------------------------------------------------------------------------------- // }}} // namespace Nuclex::Support::Events #endif // NUCLEX_SUPPORT_EVENTS_DELEGATE_H