C ++ 11 and event handling

    I think that event handling as a way of interacting objects in OOP is known to almost everyone who has ever touched OOP. At least, this approach is very convenient in a very wide, in my opinion, range of tasks. In many programming languages, an event-handling mechanism is built-in; however, in C ++ there is no such mechanism. Let's see what can be done about it.

    Brief introduction


    An event is something that can happen to a certain object under certain conditions (for example, a button when you click on it with the mouse). Other objects may need to be aware of this; then they subscribe to the event . In this case, when an event occurs, the handler of a third-party object subscribed to the event is called ; thus, it is possible for him to execute some code, i.e. respond to the event. Similarly, an object can unsubscribe from an event if it does not want to respond to it anymore. As a result, we have a lot of objects that can be connected with each other through the events of one of them and the reaction to these events of others.

    Somehow, although everyone knows this.

    The simplest implementation


    It would seem to implement such behavior is easy. And it could look like this:

    template<class ...TParams>
    classAbstractEventHandler
    {public:
            virtualvoidcall( TParams... params )= 0;
        protected:    
            AbstractEventHandler() {}
    };
    

    template<class ...TParams>
    classTEvent
    {using TEventHandler = AbstractEventHandler<TParams...>;
        public:
            TEvent() :
                m_handlers()
            {
            }
            ~TEvent()
            {
                for( TEventHandler* oneHandler : m_handlers )
                    delete oneHandler;
                m_handlers.clear();
            }
            voidoperator()( TParams... params ){
                for( TEventHandler* oneHandler : m_handlers )
                    oneHandler->call( params... );
            }
            voidoperator+=( TEventHandler& eventHandler )
            {
                m_handlers.push_back( &eventHandler );
            }
        private:
            std::list<TEventHandler*> m_handlers;
    };
    

    template<classTObject, class ...TParams>
    classMethodEventHandler :public AbstractEventHandler<TParams...>
    {
        using TMethod = void( TObject::* )( TParams... );
        public:
            MethodEventHandler( TObject& object, TMethod method ) :
                AbstractEventHandler<TParams...>(),
                m_object( object ),
                m_method( method )
            {
                assert( m_method != nullptr );
            }
            virtualvoidcall( TParams... params ) override final
            {
                ( m_object.*m_method )( params... );
            }
        private:
            TObject& m_object;
            TMethod m_method;
    };
    template<classTObject, class ...TParams>
    AbstractEventHandler<TParams...>& createMethodEventHandler( TObject& object, void( TObject::*method )( TParams... ) )
    {
        return *new MethodEventHandler<TObject, TParams...>( object, method );
    }
    #define METHOD_HANDLER( Object, Method ) createMethodEventHandler( Object, &Method )#define MY_METHOD_HANDLER( Method ) METHOD_HANDLER( *this, Method )

    The application of this case should be:

    classTestWindow
    {
        . . .
        public:
            TEvent<conststd::string&, unsignedint> onButtonClick;
        . . .
    };
    classClickEventHandler
    {
        . . .
        public:
            voidtestWindowButtonClick( conststd::string&, unsignedint ){ ... }
        . . .
    };
    intmain( int argc, char *argv[] ){
        . . .
        TestWindow testWindow;
        ClickEventHandler clickEventHandler;
        testWindow.onButtonClick += METHOD_HANDLER( clickEventHandler, ClickEventHandler::testWindowButtonClick );
        . . .
    }
    

    Naturally, a handler method (a member function of a class) will not be the only type of handler, but more on that later.

    It seems everything is convenient, compact and cool. But while there are a number of flaws.

    Handler comparison


    To implement unsubscribe from an event, you must add a comparison capability to the handler (at == and ! == ). Such handlers will be considered equal if they call the same method (-function-member of a class) of the same object (that is, the same instance of the same class).

    template<class ...TParams>
    classAbstractEventHandler
    {
        . . .
        using MyType = AbstractEventHandler<TParams...>;
        public:
            booloperator==( const MyType& other ) const
            {
                return isEquals( other );
            }
            booloperator!=( const MyType& other ) const
            {
                return !( *this == other );
            }
        protected:
            virtualboolisEquals( const MyType& other )const= 0;
        . . .
    };
    

    template<classTMethodHolder, class ...TParams>
    classMethodEventHandler :public AbstractEventHandler<TParams...>
    {
        . . .
        using TMethod = void( TObject::* )( TParams... );
        protected:
            virtualboolisEquals( const AbstractEventHandler<TParams...>& other )const override
            {
                const MyType* _other = dynamic_cast<const MyType*>( &other );
                return ( _other != nullptr && &m_object == &_other.m_object && m_method == _other.m_method );
            }
        private:
            TObject& m_object;
            TMethod m_method;
        . . .
    };
    

    Then we will be able to remove handlers from the event subscription. In this case, it is necessary to prohibit adding the same (equal) handlers.

    template<class ...TParams>
    classTEvent
    {
        . . .
        using TEventHandler = AbstractEventHandler<TParams...>;
        using TEventHandlerIt = typenamestd::list<TEventHandler*>::const_iterator;
        public:
            booloperator+=( TEventHandler& eventHandler )
            {
                if( findEventHandler( eventHandler ) == m_handlers.end() )
                {
                    m_handlers.push_back( &eventHandler );
                    returntrue;
                }
                returnfalse;
            }
            booloperator-=( TEventHandler& eventHandler )
            {
                auto it = findEventHandler( eventHandler );
                if( it != m_handlers.end() )
                {
                    TEventHandler* removedEventHandler = *it;
                    m_handlers.erase( it );
                    delete removedEventHandler;
                    returntrue;
                }
                returnfalse;
            }
        private:
            inline TEventHandlerIt findEventHandler( TEventHandler& eventHandler )const{
                returnstd::find_if( m_handlers.cbegin(), m_handlers.cend(), [ &eventHandler ]( const TEventHandler* oneHandler )
                {
                    return ( *oneHandler == eventHandler );
                } );
            }
            std::list<TEventHandler*> m_handlers;
        . . .
    };
    

    Here, the handler's add / remove functions return true if successful, and false if the corresponding action (add or remove) was not performed.

    Yes, the use case with comparison implies the creation of temporary, not added handlers, which are not deleted anywhere. But more about that later.

    Can I use it? Not yet fully.

    Delete handler inside handler


    So, we immediately encounter a crash when executing the code, where the handler itself writes itself off from the event (I think it's not the rarest use case when the handler self-spits under any conditions):

    classTestWindow
    {
        . . .
        public:
            TEvent<conststd::string&, unsignedint> onButtonClick;
            static TestWindow& instance();
        . . .
    };
    classClickEventHandler
    {
        . . .
        public:
            voidtestWindowButtonClick( conststd::string&, unsignedint ){
                TestWindow::instance().onButtonClick -= MY_METHOD_HANDLER( ClickEventHandler::testWindowButtonClick );
            }
        . . .
    };
    intmain( int argc, char *argv[] ){
        . . .
        ClickEventHandler clickEventHandler;
        TestWindow::instance().onButtonClick += METHOD_HANDLER( clickEventHandler, ClickEventHandler::testWindowButtonClick );
        . . .
    }
    

    The problem arises for a very simple reason:

    • the event is triggered and starts iterating through (with the help of iterators) handlers, calling them;
    • another handler internally causes deletion itself;
    • event removes the given handler, making the corresponding iterator invalid;
    • after this handler is completed, the event returns to the rest, but the current iterator (corresponding to the remote handler) is no longer valid;
    • an event tries to access an invalid iterator, causing a crash.

    Therefore, it is necessary to check the cases when the list of handlers can be modified, which would lead to the invalidity of the iterators; and then implement read protection for such iterators.

    The advantage of std :: list 'and in this application is the fact that when deleting it makes invalid only one iterator - per remote element (affecting, for example, the subsequent elements); and adding an element does not lead to any iterators being invalid. Thus, we need to check a single case: the removal of an element whose iterator is current in the current iteration of elements. In this case, you can, for example, not delete the element, but simply mark that the current element is to be deleted, and let it be done inside the search of elements.

    It would be possible to immediately roll out the implementation of this, but I propose to solve this problem together with the following.

    Thread safety


    Potentially, calls to three possible functions — add, delete, and iterate (when an event fires) handlers — are possible from different threads at random points in time. This creates a whole field of opportunities for their "intersection" in time, "overlaying" their performance on each other and the fall of the program in the end. Try to avoid this; mutexes are our everything .

    template<class ...TParams>
    classTEvent
    {using TEventHandler = AbstractEventHandler<TParams...>;
        using TEventHandlerIt = typenamestd::list<TEventHandler*>::const_iterator;
        public:
            TEvent() :
                m_handlers(),
                m_currentIt(),
                m_isCurrentItRemoved( false ),
                m_handlerListMutex()
            {
            }
            voidoperator()( TParams... params ){
                m_handlerListMutex.lock_shared();
                m_isCurrentItRemoved = false;
                m_currentIt = m_handlers.begin();
                while( m_currentIt != m_handlers.end() )
                {
                    m_handlerListMutex.unlock_shared();
                    ( *m_currentIt )->call( params... );
                    m_handlerListMutex.lock_shared();
                    if( m_isCurrentItRemoved )
                    {
                        m_isCurrentItRemoved = false;
                        TEventHandlerIt removedIt = m_currentIt;
                        ++m_currentIt;
                        deleteHandler( removedIt );
                    }
                    else
                    {
                        ++m_currentIt;
                    }
                }
                m_handlerListMutex.unlock_shared();
            }
            booloperator+=( TEventHandler& eventHandler )
            {
                std::unique_lock<std::shared_mutex> _handlerListMutexLock( m_handlerListMutex );
                if( findEventHandler( eventHandler ) == m_handlers.end() )
                {
                    m_handlers.push_back( std::move( eventHandler ) );
                    returntrue;
                }
                returnfalse;
            }
            booloperator-=( TEventHandler& eventHandler )
            {
                std::unique_lock<std::shared_mutex> _handlerListMutexLock( m_handlerListMutex );
                auto it = findEventHandler( eventHandler );
                if( it != m_handlers.end() )
                {
                    if( it == m_currentIt )
                        m_isCurrentItRemoved = true;
                    else
                        deleteHandler( it );
                    returntrue;
                }
                returnfalse;
            }
        private:      
            // использовать под залоченным для чтения 'm_handlerListMutex'inline TEventHandlerIt findEventHandler( TEventHandler& eventHandler )const{
                returnstd::find_if( m_handlers.cbegin(), m_handlers.cend(), [ &eventHandler ]( const TEventHandler* oneHandler )
                {
                    return ( *oneHandler == eventHandler );
                } );
            }
            // использовать под залоченным для записи 'm_handlerListMutex'inlinevoiddeleteHandler( TEventHandlerIt it ){
                TEventHandler* removedEventHandler = *it;
                m_handlers.erase( it );
                delete removedEventHandler;
            }
            std::list<TEventHandler*> m_handlers;
            // использовать под залоченным 'm_handlerListMutex'mutable TEventHandlerIt m_currentIt;
            mutablebool m_isCurrentItRemoved;
            mutablestd::shared_mutex m_handlerListMutex;
    };
    

    Do not forget to leave the "window" nezalochennoti when calling each handler. This is necessary so that inside the handler you can access the event and modify it (for example, add / remove handlers) without causing a deadlock . You should not be afraid of the validity of the data, because, as we found out, the only thing that leads to this is the deletion of the current element, and this situation is handled.
    UPD1. Thanks Cheater , suggested that std :: shared_mutex appears only in C ++ 17 (and std :: shared_lock only in C ++ 14 ). Those for whom it is critical, apparently, will have to do with std :: mutex .
    UPD2.Further, about thread safety (without preserving the sequence of narration).

    Event visibility problem


    When using an event as a member of a class, it seems logical to make it appear public so that third-party objects can add / remove their handlers. However, this will result in operator () , i.e. the event call will also be accessible from the outside, which in some cases may be unacceptable. Let's solve this problem by extracting from the event class ( TEvent <...> ) an abstract interface intended only for operating handlers.

    template<class ...TParams>
    classIEvent
    {protected:
            using TEventHandler = AbstractEventHandler<TParams...>;
        public:
            booloperator+=( TEventHandler& eventHandler )
            {
               return addHandler( eventHandler );
            }
            booloperator-=( TEventHandler& eventHandler )
            {
                return removeHandler( eventHandler );
            }
        protected:
            IEvent() {}
            virtualbooladdHandler( TEventHandler& eventHandler )= 0;
            virtualboolremoveHandler( TEventHandler& eventHandler )= 0;
    };
    

    template<class ...TParams>
    classTEvent :public IEvent<TParams...>
    {
        . . .
        public:
            TEvent() :
                IEvent<TParams...>()
                . . .
            {
            }
        protected:
            virtualbooladdHandler( TEventHandler& eventHandler ) override
            {
                // код, который был ранее в 'TEvent::operator+='
            }
            virtualboolremoveHandler( TEventHandler& eventHandler ) override
            {
                // код, который был ранее в 'TEvent::operator-='
            }
        . . .
    };
    

    Now we can smash into different areas of visibility the part of the event responsible for working with handlers, and the part responsible for calling it.

    classTestWindow
    {
        . . .
        public:
            TestWindow() :
                onButtonClick( m_onButtonClick ),
                m_onButtonClick()
            {
            }
            IEvent<conststd::string&, unsignedint>& onButtonClick;
        protected:
            TEvent<conststd::string&, unsignedint> m_onButtonClick;
        . . .
    };
    

    Thus, now third-party objects can add / remove their handlers via TestWindow :: onButtonClick , however, they will not be able to trigger this event themselves. The call can now be made only inside the class TestWindow (and its descendants, if the scope of the event, as an example, is protected ).

    The trivial code gradually begins to turn into something monstrous, but this is not the end.

    Matching the parameters of the event and its handlers


    In the current implementation, the event and any event handler must have a strictly corresponding list of parameters. This leads to several disadvantages.

    The first. Suppose we have a class template in which there is an event with a template parameter.

    template<classTSource>
    classMyClass
    {
        . . .
        public:
            TEvent<const TSource&> onValueChanged;
        . . .
    };
    

    Due to the fact that the type that will be used here is unknown in advance, it makes sense to transfer it by a constant reference, and not by value. However, now for any implementation, even with fundamental types, there must be corresponding handlers.

    MyClass<bool> myBoolClass;
    . . .
    template<classTSource>
    classMyHandlerClass
    {
        . . .
        private:
            voidhandleValueChanged1( constbool& newValue );
            voidhandleValueChanged2( bool newValue );
        . . .
    };
    . . .
    MyHandlerClass myHandlerClass;
    myBoolClass.onValueChanged += METHOD_HANDLER( myHandlerClass, MyHandlerClass::handleValueChanged1 );  // OK
    myBoolClass.onValueChanged += METHOD_HANDLER( myHandlerClass, MyHandlerClass::handleValueChanged2 );  // compile error

    I would like to be able to connect with a similar event and handlers of the form MyHandlerClass :: handleValueChanged2 , but so far there is no such possibility.

    The second. Let's try to implement a handler-functor in the same way as an existing handler-method (-function-member of a class).

    template<classTFunctor, class ...TParams>
    classFunctorEventHandler :public AbstractEventHandler<TParams...>
    {
        public:
            FunctorEventHandler( TFunctor& functor ) :
                AbstractEventHandler<TParams...>(),
                m_functor( functor )
            {
            }
            virtualvoidcall( TParams... params ) override final
            {
                m_functor( params... );
            }
        private:
            TFunctor& m_functor;
    };
    template<classTFunctor, class ...TParams>
    AbstractEventHandler<TParams...>& createFunctorEventHandler( TFunctor&& functor )
    {return *new FunctorEventHandler<TFunctor, TParams...>( functor );
    }
    #define FUNCTOR_HANDLER( Functor ) createFunctorEventHandler( Functor )

    Now we will try to screw it to some event.

    classTestWindow
    {
        . . .
        public:
            TEvent<conststd::string&, unsignedint> onButtonClick;
        . . .
    };
    structClickEventHandler
    {voidoperator()( conststd::string&, unsignedint ){ . . . }
    };
    intmain( int argc, char *argv[] ){
        . . .
        TestWindow testWindow;
        ClickEventHandler clickEventHandler;
        testWindow.onButtonClick += FUNCTOR_HANDLER( clickEventHandler );
        . . .
    }
    

    The result will be a compilation error. For the createFunctorEventHandler function , the compiler cannot infer the types of TParams ... from the only argument of this function - the functor itself. The functor really does not contain any information about what type of processor you need to create on its basis. The only thing that can be done in this situation is to write something like:

    testWindow.onButtonClick += createFunctorEventHandler<ClickEventHandler, conststd::string&, unsignedint>( clickEventHandler );
    

    But you do not want to do this at all.

    Connection of event with handlers of different types


    So, there is a Wishlist, it's up to the implementation. We will consider the situation on the example of a handler-functor; a handler-method (-function-member of a class) will turn out in the same way.

    Since, based on the functor alone, it is impossible to say what the list of parameters of the corresponding handler will be, then we will not do that. This issue becomes relevant not at the time of the creation of the handler, but at the time of the attempt to join it to a specific event. And yes, these are two different points. You can implement this idea as follows:

    template<classTFunctor> classFunctorHolder;template<classTFunctor, class ...TParams>
    classFunctorEventHandler :public AbstractEventHandler<TParams...>
    {
        public:
            FunctorEventHandler( FunctorHolder<TFunctor>& functorHolder ) :
                AbstractEventHandler<TParams...>(),
                m_functorHolder( functorHolder )
            {
            }
            virtualvoidcall( TParams... params ) override
            {
                m_functorHolder.m_functor( params... );
            }    
        private:
            FunctorHolder<TFunctor>& m_functorHolder;
        . . .
    };
    

    template<classTFunctor>
    classFunctorHolder
    {public:
            FunctorHolder( TFunctor& functor ) :
                m_functor( functor )
            {
            }
            template<class ...TCallParams>
            operatorAbstractEventHandler<TCallParams...>&()
            {return *new FunctorEventHandler<TFunctor, TCallParams...>( *this );
            }
        private:
            TFunctor& m_functor;
        . . .
        template<classTFunctor, class ...TParams> friendclassFunctorEventHandler;
    };
    

    template<classTFunctor>
    FunctorHolder<TFunctor>& createFunctorEventHandler( TFunctor&& functor )
    {return *new FunctorHolder<TFunctor>( functor );
    }
    #define     FUNCTOR_HANDLER( Functor )              createFunctorEventHandler( Functor )#define     LAMBDA_HANDLER( Lambda )                FUNCTOR_HANDLER( Lambda )#define     STD_FUNCTION_HANDLER( StdFunction )     FUNCTOR_HANDLER( StdFunction )#define     FUNCTION_HANDLER( Function )            FUNCTOR_HANDLER( &Function )

    template<class ...TParams>
    classIEvent
    {protected:
            using TEventHandler = AbstractEventHandler<TParams...>;
        public:
            template<classTSome>
            booloperator+=( TSome&& some )
            {return addHandler( static_cast<TEventHandler&>( some ) );
            }
            template<classTSome>
            booloperator-=( TSome&& some )
            {return removeHandler( static_cast<TEventHandler&>( some ) );
            }
        protected:
            IEvent() {}
            virtualbooladdHandler( TEventHandler& eventHandler )= 0;
            virtualboolremoveHandler( TEventHandler& eventHandler )= 0;
    };
    

    In short, the separation of the moments of creating a handler and attaching it to an event here is more pronounced than before. This allows you to bypass the problems described in the previous paragraph. Type compatibility check will occur when trying to caste a certain FunctorHolder to a certain FunctorEventHandler , or rather, to create an instance of the class FunctorEventHandler <....> With a very specific type of functor; and in this class there will be a line of code m_functorHolder.m_functor (params ...); , which is simply not compiled for a set of types incompatible with a functor (or if it is not a functor at all, that is, an object that does not have a operator () ).

    I repeat that the problem of deleting temporary objects will be discussed below. In addition, it is worth noting that a bunch of macros for each case is made, firstly, in order to demonstrate the capabilities of this type of handler, and secondly, in case of possible modification by a file of any of them.

    Check the result.

    classTestWindow
    {
        . . .
        public:
            TEvent<conststd::string&, unsignedint> onButtonClick;
        . . .
    };
    structFunctor
    {voidoperator()( conststd::string&, unsignedint ){}
    };
    structFunctor2
    {voidoperator()( std::string, unsignedint ){}
    };
    structFunctor3
    {voidoperator()( conststd::string&, constunsignedint& ){}
    };
    structFunctor4
    {voidoperator()( std::string, constunsignedint& ){}
    };
    structFunctor5
    {voidoperator()( std::string&, unsignedint& ){}
    };
    structFunctor6
    {voidoperator()( conststd::string&, unsignedint& ){}
    };
    structFunctor7
    {voidoperator()( std::string&, constunsignedint& ){}
    };
    intmain( int argc, char *argv[] ){
        . . .
        TestWindow testWindow;
        Functor functor;
        Functor2 functor2;
        Functor3 functor3;
        Functor4 functor4;
        Functor5 functor5;
        Functor6 functor6;
        Functor7 functor7;
        testWindow.onButtonClick += FUNCTOR_HANDLER( functor );     // ok
        testWindow.onButtonClick += FUNCTOR_HANDLER( functor2 );    // ok
        testWindow.onButtonClick += FUNCTOR_HANDLER( functor3 );    // ok
        testWindow.onButtonClick += FUNCTOR_HANDLER( functor4 );    // ok
        testWindow.onButtonClick += FUNCTOR_HANDLER( functor5 );    // compile error
        testWindow.onButtonClick += FUNCTOR_HANDLER( functor6 );    // ok
        testWindow.onButtonClick += FUNCTOR_HANDLER( functor7 );    // compile error
        . . .
    }
    

    A compilation error occurs when trying to convert one of the parameters from const lvalue to lvalue . Converting from rvalue to unconst lvalue does not cause an error, although it is worth noting that it creates a potential threat to a self-shot in the leg: the handler will be able to change the variable copied to the stack, which will happily be removed when it leaves this handler.

    In general, the error message should look something like this:

    Error	C2664	'void Functor5::operator ()(std::string &,unsigned int &)': cannot convert argument 1 from 'const std::string' to'std::string &'
    

    For greater clarity, when using events and handlers in third-party code, you can add your own error message. This will require writing a small auxiliary structure (I confess, I spotted a similar approach somewhere):

    namespace
    {
        template<classTFunctor, class ...TParams>
        structIsFunctorParamsCompatible
        {private:
                template<classTCheckedFunctor, class ...TCheckedParams>
                staticconstexprstd::true_type exists( decltype( std::declval<TCheckedFunctor>()( std::declval<TCheckedParams>()... ) )* = nullptr );
                template<classTCheckedFunctor, class ...TCheckedParams>
                staticconstexprstd::false_type exists( ... );
            public:
                staticconstexprbool value = decltype( exists<TFunctor, TParams...>( nullptr ) )::value;
        };
    } //

    template<classTFunctor, class ...TParams>
    classFunctorEventHandler :public AbstractEventHandler<TParams...>
    {
        . . .
        public:
            virtualvoidcall( TParams... params ) override
            {
                static_assert( IsFunctorParamsCompatible<TFunctor, TParams...>::value, "Event and functor arguments are not compatible" );
                m_functorHolder->m_functor( params... );
            }
        . . .
    };
    

    The work of this case is based on the mechanism of SFINAE . In short, an attempt is made to compile the first exists function , however, if this fails due to the incompatibility of the arguments (or the lack of operator () of what is passed as a functor), the compiler does not throw an error, but simply tries to compile the second function; we do everything so that its compilation is always successful, and then, in fact, which of the functions was compiled, we conclude (writing the result in value ) about the compatibility of the arguments for the given types.

    Now the error message will look like this:

    Error	C2338	Event and functor arguments are not compatible
    Error	C2664	'void Functor5::operator ()(std::string &,unsigned int &)': cannot convert argument 1 from 'const std::string' to 'std::string &'

    In addition to an additional, more informative error message, this approach solves the problem of converting the argument (s) from rvalue to unconst lvalue : now it causes an error of incompatible arguments, i.e. an attempt to add the functor6 handler from the example above results in a compile-time error.
    UPD. Revision (without preserving the sequence of the story).

    Comparison of functors


    Due to changes in the class handler, the implementation of comparison of instances of this class will change a little. Again, I will only give the implementation of a handler-functor, because a handler-method (-function-member of a class) will look similar.

    template<class ...TParams>
    classAbstractEventHandler
    {
        . . .
        using MyType = AbstractEventHandler<TParams...>;
        public:
            booloperator==( const MyType& other ) const
            {
                return isEquals( other );
            }
            booloperator!=( const MyType& other ) const
            {
                return !( *this == other );
            }
        protected:
            virtualboolisEquals( const MyType& other )const= 0;
        . . .
    };
    

    template<classTFunctor, class ...TParams>
    classFunctorEventHandler :public AbstractEventHandler<TParams...>
    {
        . . .
        using MyType = FunctorEventHandler<TFunctor, TParams...>;
        protected:
            virtualboolisEquals( const AbstractEventHandler<TParams...>& other )const override
            {
                const MyType* _other = dynamic_cast<const MyType*>( &other );
                return ( _other != nullptr && *m_functorHolder == *_other->m_functorHolder );
            }
        private:
            FunctorHolder<TFunctor>& m_functorHolder;
        . . .
    };
    

    template<classTFunctor>
    classFunctorHolder
    {
        . . .
        using MyType = FunctorHolder<TFunctor>;
        public:
            booloperator==( const MyType& other ) const
            {
                return ( m_functor == other.m_functor );
            }
            booloperator!=( const MyType& other ) const
            {
                return !( *this == other );
            }
        private:
            TFunctor& m_functor;
        . . .
    };
    

    At this similarity in the implementation of the comparison ends and begins part only for handlers-functors.

    As noted above, we have obtained several types of handlers-functors: directly objects-functors, lambda expressions, instances of the class std :: function , separate functions. Of these, functor objects, lambda expressions, and instances of the std :: function class cannot be compared using operator == (they need to be compared by address), but individual functions can, because already stored at. In order not to rewrite the comparison function separately for each case, we write it in a general form:

    namespace
    {
        template<classTEqu, classTEnabled = void>
        structEqualityChecker;template<classTEquatable>
        structEqualityChecker<TEquatable, typename std::enable_if<is_equatable<TEquatable>::value>::type>
        {staticconstexprboolisEquals( const TEquatable& operand1, const TEquatable& operand2 ){
                return ( operand1 == operand2 );
            }
        };
        template<classTNonEquatable>
        structEqualityChecker<TNonEquatable, typename std::enable_if<!is_equatable<TNonEquatable>::value>::type>
        {staticconstexprboolisEquals( const TNonEquatable& operand1, const TNonEquatable& operand2 ){
                return ( &operand1 == &operand2 );
            }
        };
    } //template<classTFunctor>
    classFunctorHolder
    {
        . . .
        using MyType = FunctorHolder<TFunctor>;
        public:
            booloperator==( const MyType& other ) const
            {
                return EqualityChecker<TFunctor>::isEquals( m_functor, other.m_functor );
            }
        private:
            TFunctor& m_functor;
        . . .
    };
    

    It is implied that is_equatable is an auxiliary template that determines whether two instances of a given type can be checked for equality. With it, using std :: enable_if , we choose one of two partially specialized structures EqualityChecker , which will make the comparison: by value or by address. Implemented is_equatable it can be as follows:

    template<classT>
    classis_equatable
    {private:
            template<classU>
            staticconstexprstd::true_type exists( decltype( std::declval<U>() == std::declval<U>() )* = nullptr );
            template<classU>
            staticconstexprstd::false_type exists( ... );
        public:
            staticconstexprbool value = decltype( exists<T>( nullptr ) )::value;
    };
    

    This implementation is based on the SFINAE mechanism that was previously used . Only here we check the availability of operator == for instances of a given class.

    This simple way of implementing the comparison of handlers-functors is ready.

    Garbage collection


    Be indulgent, I also wanted to insert a loud headline.

    We are approaching the final, and it’s time to get rid of the huge number of objects that are being created that no one controls.

    At each event of the event with the handler, two objects are created: Holder , which stores the executable part of the handler, and EventHandlerassociating it with an event. Let's not forget that in the case of an attempt to re-add the handler, no addition will occur - two objects “hung in the air” (unless, of course, separately check this case each time). Another situation: delete handler; two new objects are also created to search for the same (equal) in the list of event handlers; The found handler from the list, of course, is deleted (if it exists), and this temporary, created for search and consisting of two objects, is again “in the air”. In general, not cool.

    Turn to smart pointers . It is necessary to determine what semantics of ownership of each of the two handler objects will be: sole ownership ( std :: unique_ptr ) or shared ( std :: shared_ptr ).

    Holderbesides using the event itself when adding / deleting should be stored in the EventHandler , therefore we use for shared ownership, and for the EventHandler , the sole ownership , since after creation, it will be stored only in the list of event handlers.

    We implement this idea:

    template<class ...TParams>
    classAbstractEventHandler
    {
        . . .
        public:
            virtual ~AbstractEventHandler() {}
        . . .
    };
    template<class ...Types>
    usingTHandlerPtr = std::unique_ptr<AbstractEventHandler<Types...>>;
    

    namespace
    {
        template<classTSome>
        structHandlerCast
        {template<class ...Types>
            staticconstexprTHandlerPtr<Types...> cast( TSome& some )
            {returnstatic_cast<THandlerPtr<Types...>>( some );
            }
        };
        template<classTPtr>
        structHandlerCast<std::shared_ptr<TPtr>>
        {template<class ...Types>
            staticconstexprTHandlerPtr<Types...> cast( std::shared_ptr<TPtr> some )
            {
                return HandlerCast<TPtr>::cast<Types...>( *some );
            }
        };
    } //template<class ...TParams>
    classIEvent
    {public:
            template<classTSome>
            booloperator+=( TSome&& some )
            {return addHandler( HandlerCast<TSome>::cast<TParams...>( some ) );
            }
            template<classTSome>
            booloperator-=( TSome&& some )
            {return removeHandler( HandlerCast<TSome>::cast<TParams...>( some ) );
            }
        protected:
            using TEventHandlerPtr = THandlerPtr<TParams...>;
            IEvent() {}
            virtualbooladdHandler( TEventHandlerPtr eventHandler )= 0;
            virtualboolremoveHandler( TEventHandlerPtr eventHandler )= 0;
    };
    template<class ...TParams>
    classTEvent :public IEvent<TParams...>
    {
        using TEventHandlerIt = typenamestd::list<TEventHandlerPtr>::const_iterator;
        public:
            TEvent()
            {
                . . .
            }
            ~TEvent()
            {
                // empty
            }
        protected:
            virtualbooladdHandler( TEventHandlerPtr eventHandler ) override
            {
                std::unique_lock<std::shared_mutex> _handlerListMutexLock( m_handlerListMutex );
                if( findEventHandler( eventHandler ) == m_handlers.end() )
                {
                    m_handlers.push_back( std::move( eventHandler ) );
                    returntrue;
                }
                returnfalse;
            }
            virtualboolremoveHandler( TEventHandlerPtr eventHandler ) override
            {
                . . .
            }
        private:
            // использовать под залоченным для чтения 'm_handlerListMutex'inline TEventHandlerIt findEventHandler( const TEventHandlerPtr& eventHandler )const{
                returnstd::find_if( m_handlers.cbegin(), m_handlers.cend(), [ &eventHandler ]( const TEventHandlerPtr& oneHandler )
                {
                    return ( *oneHandler == *eventHandler );
                } );
            }
            // использовать под залоченным для записи 'm_handlerListMutex'inlinevoiddeleteHandler( TEventHandlerIt it ){
                m_handlers.erase( it );
            }
            std::list<TEventHandlerPtr> m_handlers;
        . . .
    };
    

    template<classTMethodHolder, class ...TParams>
    classMethodEventHandler :public AbstractEventHandler<TParams...>
    {
        . . .
        using TMethodHolderPtr = std::shared_ptr<TMethodHolder>;
        public:
            MethodEventHandler( TMethodHolderPtr methodHolder ) :
                AbstractEventHandler<TParams...>(),
                m_methodHolder( methodHolder )
            {
                assert( m_methodHolder != nullptr );
            }
        private:
            TMethodHolderPtr m_methodHolder;
        . . .
    };
    template<classTObject, class ...TParams>
    classMethodHolder
    {using MyType = MethodHolder<TObject, TParams...>;
        using TMethod = void( TObject::* )( TParams... );
        public:
            MethodHolder( TObject& object, TMethod method )
            {
                . . .
            }
            template<class ...TCallParams>
            operatorTHandlerPtr<TCallParams...>()
            {return THandlerPtr<TCallParams...>( new MethodEventHandler<MyType, TCallParams...>( /* ЧТО СЮДА ПЕРЕДАТЬ? */ ) );
            }
        . . .
    };
    template<classTObject, class ...TParams>
    std::shared_ptr<MethodHolder<TObject, TParams...>> createMethodEventHandler( TObject& object, void( TObject::*method )( TParams... ) )
    {
        returnstd::shared_ptr<MethodHolder<TObject, TParams...>>( new MethodHolder<TObject, TParams...>( object, method ) );
    }
    #define METHOD_HANDLER( Object, Method ) createMethodEventHandler( Object, &Method )#define MY_METHOD_HANDLER( Method ) METHOD_HANDLER( *this, Method )

    Everything in order.

    To start the event and its interface for working with handlers. In the latter, converting types by directly using static_cast will no longer work, because the type to be converted lies “inside” std :: shared_ptr . Now for such a transformation we will use the auxiliary structure of HandlerCast , which by its private specialization will provide access to the object inside std :: shared_ptr , and already working with it (in its non-specialized implementation), apply the good old static_cast .

    The event itself; There are some important changes here too. First, let's stop manually deleting instances of handlers in the destructor and during deletion; now it is enough to remove from the list a smart pointer with this handler. In addition, when adding a handler, it is important not to forget std :: move , since std :: unique_ptr does not support copying (which is quite logical for such semantics).

    We turn to the handlers. According to the old tradition, only one of them is given, the second is similar. And here, at first glance, it all comes down to changing the types of stored / created objects with links / pointers to smart pointers.

    But there is one subtle point. The createMethodEventHandler function will return std :: shared_ptr to the instance.MethodHolder . A little later, an attempt will be made to convert it to a handler type ( MethodEventHandler ), where it will have to create a new MethodEventHandler instance , passing it to the constructor std :: shared_ptr to itself. That was how it was intended that the MethodHolder instance would later retire when the MethodEventHandler instance was deleted . But the problem is that MethodHolder does not have access to the already created std :: shared_ptr that stores it.

    To solve the problem, you will have to store a smart pointer to yourself in MethodHolder . However, so that he does not affect his removal, we usestd :: weak_ptr :

    template<classTObject, class ...TParams>
    classMethodHolder
    {using MyType = MethodHolder<TObject, TParams...>;
        using TMethod = void( TObject::* )( TParams... );
        public:
            template<class ...TCallParams>
            operatorTHandlerPtr<TCallParams...>()
            {return THandlerPtr<TCallParams...>( new MethodEventHandler<MyType, TCallParams...>( m_me.lock() ) );
            }
            template<classTObject, class ...TParams>
            staticstd::shared_ptr<MyType> create( TObject& object, TMethod method )
            {
                std::shared_ptr<MyType> result( new MyType( object, method ) );
                result->m_me = result;
                return result;
            }
        private:
            MethodHolder( TObject& object, TMethod method ) :
                m_object( object ),
                m_method( method )
            {
                assert( m_method != nullptr );
            }
            TObject& m_object;
            TMethod m_method;
            std::weak_ptr<MyType> m_me;
    };
    template<classTObject, class ...TParams>
    std::shared_ptr<MethodHolder<TObject, TParams...>> createMethodEventHandler( TObject& object, void( TObject::*method )( TParams... ) )
    {
        return MethodHolder<TObject, TParams...>::create( object, method );
    }
    

    For better clarity, I’ll give an approximate order of events when removing a handler from an event (my apologies for the random pun):

    • an event deletes an element from the list ( m_handlers.erase (it); ), which causes its destructor to be called;
    • destructor std :: unique_ptr is called , which leads to a call to the destructor of the managed object;
    • The MethodEventHandler destructor is called , which removes all object fields, including the m_methodHolder field , which is std :: shared_ptr ;
    • destructor std :: shared_ptr is called ; he sees that the owners counter has reached zero (because he was the only owner at the time of removal from the event) and calls the managed object destructor ( MethodHolder ); However, the destruction of the control unit is not called, because the reference counter std :: weak_ptr is not yet zero;
    • the MethodHolder destructor is called , which leads to the destruction of all fields, including the m_me field , which is std :: weak_ptr ;
    • destructor std :: weak_ptr is called ; his managed object has already been destroyed; because the reference counter std :: weak_ptr became equal to zero, the destruction of the control unit is called;
    • profit

    It is important to remember that the destructor of the AbstractEventHandler class must be virtual; otherwise, after point 2 in point 3 , the AbstractEventHandler destructor will be called and no further actions will be performed.

    Event and handler connection


    In some cases, when adding / removing a single handler from an event occurs quite often (according to some logic), I don’t want to mess around, taking an instance of the event and an instance of the handler each time to once again implement a subscription / unsubscribe from this event. And you want to connect them once, and then, if necessary, work with this connection, adding / removing with it the predefined handler from a predetermined event. You can implement this as follows:

    template<class ...Types>
    usingTHandlerPtr = std::shared_ptr<AbstractEventHandler<Types...>>;
    

    template<class ...TParams>
    classIEvent
    {
        . . .
        protected:
            using TEventHandlerPtr = THandlerPtr<TParams...>;
            virtualboolisHandlerAdded( const TEventHandlerPtr& eventHandler )const= 0;
            virtualbooladdHandler( TEventHandlerPtr eventHandler )= 0;
            virtualboolremoveHandler( TEventHandlerPtr eventHandler )= 0;
        friendclassHandlerEventJoin<TParams...>;
        . . .
    };
    template<class ...TParams>
    classTEvent :public IEvent<TParams...>
    {
        . . .
        protected:
            virtualboolisHandlerAdded( const TEventHandlerPtr& eventHandler )const override
            {
                std::shared_lock<std::shared_mutex> _handlerListMutexLock( m_handlerListMutex );
                return ( findEventHandler( eventHandler ) != m_handlers.end() );
            }
            virtualbooladdHandler( TEventHandlerPtr eventHandler ) override { . . . }
            virtualboolremoveHandler( TEventHandlerPtr eventHandler ) override { . . . }
        private:
            // использовать под залоченным для чтения 'm_handlerListMutex'inline TEventHandlerIt findEventHandler( const TEventHandlerPtr& eventHandler )const{ . . . }
            std::list<TEventHandlerPtr> m_handlers;
            mutablestd::shared_mutex m_handlerListMutex;
        . . .
    };
    

    template<class ...TParams>
    classHandlerEventJoin
    {public:
            HandlerEventJoin( IEvent<TParams...>& _event, THandlerPtr<TParams...> handler ) :
                m_event( _event ),
                m_handler( handler )
            {
            }
            inlineboolisJoined()const{
                return m_event.isHandlerAdded( m_handler );
            }
            inlinebooljoin(){
                return m_event.addHandler( m_handler );        
            }
            inlineboolunjoin(){
                return m_event.removeHandler( m_handler );        
            }
        private:
            IEvent<TParams...>& m_event;
            THandlerPtr<TParams...> m_handler;
    };
    

    As you can see, now we have added another possible storage location for the handler instance, so we will use std :: shared_ptr instead of std :: unique_ptr for this .

    However, this class, as for me, is slightly inconvenient to use. It would be desirable to store and create instances of connections without a list of parameters instantiating a class template.

    We do this with the help of an abstract ancestor class and a wrapper:

    classAbstractEventJoin
    {public:
            virtual ~AbstractEventJoin() {}
            virtualboolisJoined()const= 0;
            virtualbooljoin()= 0;
            virtualboolunjoin()= 0;
        protected:
            AbstractEventJoin() {}
    };
    

    template<class ...TParams>
    classHandlerEventJoin :public AbstractEventJoin
    {
        . . .
        public:
            virtualinlineboolisJoined()const override { . . . }
            virtualinlinebooljoin() override { . . . }
            virtualinlineboolunjoin() override { . . . }
        . . .
    };
    

    classEventJoinWrapper
    {public:
            template<classTSome, class ...TParams>
            inlineEventJoinWrapper( IEvent<TParams...>& _event, TSome&& handler ) :
                m_eventJoin( std::make_shared<HandlerEventJoin<TParams...>>( _event, HandlerCast<TSome>::cast<TParams...>( handler ) ) )
            {
            }
            constexprEventJoinWrapper() :
                m_eventJoin( nullptr ){
            }
            ~EventJoinWrapper()
            {
                if( m_eventJoin != nullptr )
                    delete m_eventJoin;
            }
            operatorbool()const{
                return isJoined();
            }
            boolisAssigned()const{
                return ( m_eventJoin != nullptr );
            }
            boolisJoined()const{
                return ( m_eventJoin != nullptr && m_eventJoin->isJoined() );
            }
            booljoin(){
                return ( m_eventJoin != nullptr ? m_eventJoin->join() : false );
            }
            boolunjoin(){
                return ( m_eventJoin != nullptr ? m_eventJoin->unjoin() : false );
            }
        private:
            AbstractEventJoin* m_eventJoin;
    };
    using EventJoin = EventJoinWrapper;
    

    HandlerCast is the same supporting structure that was used here . By the way, it is important not to forget to make the AbstractEventJoin destructor virtual so that when you delete its instance in the EventJoinWrapper destructor, the HandlerEventJoin destructor is called , otherwise the THandlerPtr field and, therefore, the handler itself will not be destroyed .

    This implementation seems to work, but only at first glance. Copying or moving an EventJoinWrapper instance will re-delete m_eventJoin in its destructor. Therefore, we use std :: shared_ptr to store the instance.AbstractEventJoin , as well as implement slightly optimized motion semantics (and copying), since this will be a potentially frequent operation.

    classEventJoinWrapper
    {public:
            EventJoinWrapper( EventJoinWrapper&& other ) :
                m_eventJoin( std::move( other.m_eventJoin ) )
            {
            }
            EventJoinWrapper( EventJoinWrapper& other ) :
                m_eventJoin( other.m_eventJoin )
            {
            }
            ~EventJoinWrapper() { /*empty*/ }        
            EventJoinWrapper& operator=( EventJoinWrapper&& other )
            {
                m_eventJoin = std::move( other.m_eventJoin );
                return *this;
            }
            EventJoinWrapper& operator=( const EventJoinWrapper& other )
            {
                m_eventJoin = other.m_eventJoin;
                return *this;
            }
            . . .
        private:
            std::shared_ptr<AbstractEventJoin> m_eventJoin;
    };
    

    Now, when connecting the handler to the event, you can immediately return an instance of the new connection:

    template<class ...TParams>
    classIEvent
    {
        . . .
        public:
            template<classTSome>
            EventJoinoperator+=( TSome&& some )
            {EventJoin result( *this, std::forward<TSome>( some ) );
                result.join();
                return result;
            }
        . . .
    };
    

    And after settling the triangular dependencies by include (IEvent <= EventJointWrapper.hpp; EventJointWrapper <= HandlerEventJoin.hpp; HandlerEventJoin <= IEvent.hpp) you can even work with splitting some files into .h and .hpp .

    The creation of connection instances occurs according to the same rules as when a handler is subscribed to an event:

    structEventHolder
    {
        TEvent<conststd::string&> onEvent;
    };
    structMethodsHolder
    {voidmethod1( conststd::string& ){}
        voidmethod2( std::string ){}
        voidmethod3( std::string&& ){}
        voidmethod4( std::string& ){}
        voidmethod5( constint& ){}
    };
    intmain( int argc, char* argv[] ){
        EventHolder _eventHolder;
        MethodsHolder _methodsHolder;
        EventJoin join1 = EventJoin( _eventHolder.onEvent, METHOD_HANDLER( _methodsHolder, MethodsHolder::method1 ) );  // ok
        EventJoin join2 = EventJoin( _eventHolder.onEvent, METHOD_HANDLER( _methodsHolder, MethodsHolder::method2 ) );  // ok
        EventJoin join3 = EventJoin( _eventHolder.onEvent, METHOD_HANDLER( _methodsHolder, MethodsHolder::method3 ) );  // error
        EventJoin join4 = EventJoin( _eventHolder.onEvent, METHOD_HANDLER( _methodsHolder, MethodsHolder::method4 ) );  // error
        EventJoin join5 = EventJoin( _eventHolder.onEvent, METHOD_HANDLER( _methodsHolder, MethodsHolder::method5 ) );  // errorreturn0;
    }
    

    Plus, you can “turn on” / “turn off” event handling (for which, in principle, connections were crea

    Also popular now: