C++11 и обработка событий

    Думаю, обработка событий как способ взаимодействия объектов в ООП, известен почти каждому, кто вообще хоть раз касался ООП. По крайнее мере, такой подход весьма удобен в весьма широком, на мой взгляд, спектре задач. Во многих языках программирования механизм обработки событий является встроенным; однако в C++ такого механизма нет. Давайте посмотрим, что можно с этим сделать.

    Краткое введение


    Событие — это то, что может случиться с некоторым объектом при определённых условиях (например, с кнопкой при клике на неё мышкой). Другим объектам, возможно, необходимо знать об этом; тогда они подписываются на событие. В этом случае при возникновении события вызывается обработчик стороннего подписанного на событие объекта; таким образом, у него появляется возможность выполнить некоторый код, т.е. отреагировать на событие. Аналогично, объект может отписаться от события, если не хочет более на него реагировать. В результате мы имеем множество объектов, которые могут быть связаны друг с другом при помощи событий одних из них и реакции на эти события других.

    Как-то так, хотя это и так все знают.

    Простейшая реализация


    Казалось бы, реализовать подобное поведение несложно. И это могло бы выглядеть так:

    template<class ...TParams>
    class AbstractEventHandler
    {
        public:
            virtual void call( TParams... params ) = 0;
        protected:    
            AbstractEventHandler() {}
    };
    

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

    template<class TObject, class ...TParams>
    class MethodEventHandler : 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 );
            }
            virtual void call( TParams... params ) override final
            {
                ( m_object.*m_method )( params... );
            }
        private:
            TObject& m_object;
            TMethod m_method;
    };
    
    template<class TObject, 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 )
    

    Применение этого дела должно иметь вид:

    class TestWindow
    {
        . . .
        public:
            TEvent<const std::string&, unsigned int> onButtonClick;
        . . .
    };
    class ClickEventHandler
    {
        . . .
        public:
            void testWindowButtonClick( const std::string&, unsigned int ) { ... }
        . . .
    };
    int main( int argc, char *argv[] )
    {
        . . .
        TestWindow testWindow;
        ClickEventHandler clickEventHandler;
    
        testWindow.onButtonClick += METHOD_HANDLER( clickEventHandler, ClickEventHandler::testWindowButtonClick );
        . . .
    }
    

    Естественно, обработчик-метод (-функция-член класса) не будет единственным типом обработчиков, но об этом позже.

    Кажется, всё удобно, компактно и здорово. Но пока есть ряд недоработок.

    Сравнение обработчиков


    Чтобы реализовать отписку от события, необходимо добавить в обработчик возможность сравнения (на == и !==). Равными будут считаться такие обработчики, которые вызывают один и тот же метод (-функцию-член класса) одного и того же объекта (т.е. одного и того же экземпляра одного и того же класса).

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

    template<class TMethodHolder, class ...TParams>
    class MethodEventHandler : public AbstractEventHandler<TParams...>
    {
        . . .
        using TMethod = void( TObject::* )( TParams... );
        protected:
            virtual bool isEquals( 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;
        . . .
    };
    

    Тогда у нас появится возможность удалять обработчики из подписки события. В таком случае необходимо запретить добавлять одинаковые (равные) обработчики.

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

    Здесь функции добавления/удаления обработчика возвращают true в случае успешного выполнения и false, если соответствующее действие (добавление или удаление) не было выполнено.

    Да, вариант использования со сравнением подразумевает создание временных, никуда не добавленных обработчиков, которые нигде не удаляются. Но об этом позже.

    Можно ли этим пользоваться? Пока ещё не в полной мере.

    Удаление обработчика внутри обработчика


    Итак, сразу же сталкиваемся с падением при исполнении кода, где обработчик сам отписывает себя от события (думаю, не самый редкий use case, когда обработчик самовыпиливается при каких-либо условиях):

    class TestWindow
    {
        . . .
        public:
            TEvent<const std::string&, unsigned int> onButtonClick;
            static TestWindow& instance();
        . . .
    };
    class ClickEventHandler
    {
        . . .
        public:
            void testWindowButtonClick( const std::string&, unsigned int )
            {
                TestWindow::instance().onButtonClick -= MY_METHOD_HANDLER( ClickEventHandler::testWindowButtonClick );
            }
        . . .
    };
    int main( int argc, char *argv[] )
    {
        . . .
        ClickEventHandler clickEventHandler;
        TestWindow::instance().onButtonClick += METHOD_HANDLER( clickEventHandler, ClickEventHandler::testWindowButtonClick );
        . . .
    }
    

    Проблема возникает по весьма простой причине:

    • событие срабатывает и начинает перебирать (с помощью итераторов) обработчики, вызывая их;
    • очередной обработчик внутри себя вызывает удаление себя;
    • событие удаляет данный обработчик, делая соответствующий ему итератор невалидным;
    • после завершения данного обработчика событие возвращается к перебору остальных, однако текущий итератор (соответствующий удалённому обработчику) уже невалиден;
    • событие пытается обратиться к невалидному итератору, вызывая падение.

    Следовательно, нужно проверить случаи, когда список обработчиков может быть изменён, которые приводили бы к невалидности итераторов; и затем реализовать защиту от чтения таких итераторов.

    Преимуществом std::list'а в данном применении является тот факт, что при удалении он делает невалидным только один итератор — на удалённый элемент (на затрагивая, например, последующие); а добавление элемента вообще не приводит к невалидности каких-либо итераторов. Таким образом, нам нужно проконтролировать единственный случай: удаление элемента, итератор которого является текущим в действующем переборе элементов. В этом случае можно, например, не удалять элемент, а просто пометить, что текущий элемент подлежит удалению, и пусть это будет сделано внутри перебора элементов.

    Можно было бы сразу вкатить реализацию этого, но предлагаю решить эту проблему совместно со следующей.

    Потокобезопасность


    Потенциально, вызовы трёх возможных функций — добавления, удаления и перебора (при срабатывании события) обработчиков — возможны из разных потоков в случайные моменты времени. Это создаёт целое поле возможностей по их «пересечению» во времени, «накладыванию» их исполнения друг на друга и падению программы в итоге. Попробуем избежать этого; мьютексы — наше всё.

    template<class ...TParams>
    class TEvent
    {
        using TEventHandler = AbstractEventHandler<TParams...>;
        using TEventHandlerIt = typename std::list<TEventHandler*>::const_iterator;
        public:
            TEvent() :
                m_handlers(),
                m_currentIt(),
                m_isCurrentItRemoved( false ),
                m_handlerListMutex()
            {
            }
            void operator()( 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();
            }
            bool operator+=( TEventHandler& eventHandler )
            {
                std::unique_lock<std::shared_mutex> _handlerListMutexLock( m_handlerListMutex );
    
                if( findEventHandler( eventHandler ) == m_handlers.end() )
                {
                    m_handlers.push_back( std::move( eventHandler ) );
                    return true;
                }
                return false;
            }
            bool operator-=( 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 );
    
                    return true;
                }
                return false;
            }
        private:      
            // использовать под залоченным для чтения 'm_handlerListMutex'
            inline TEventHandlerIt findEventHandler( TEventHandler& eventHandler ) const
            {
                return std::find_if( m_handlers.cbegin(), m_handlers.cend(), [ &eventHandler ]( const TEventHandler* oneHandler )
                {
                    return ( *oneHandler == eventHandler );
                } );
            }
            // использовать под залоченным для записи 'm_handlerListMutex'
            inline void deleteHandler( TEventHandlerIt it )
            {
                TEventHandler* removedEventHandler = *it;
                m_handlers.erase( it );
                delete removedEventHandler;
            }
            
            std::list<TEventHandler*> m_handlers;
    
            // использовать под залоченным 'm_handlerListMutex'
            mutable TEventHandlerIt m_currentIt;
            mutable bool m_isCurrentItRemoved;
    
            mutable std::shared_mutex m_handlerListMutex;
    };
    

    Не забудем оставлять «окно» незалоченночти при вызове каждого обработчика. Это нужно затем, чтобы внутри обработчика можно было обращаться к событию и изменять его (например, добавлять/удалять обработчики), не вызывая deadlock. За валидность данных можно не опасаться, потому что, как мы выяснили, единственное, что к этому приводит — это удаление текущего элемента, а данная ситуация обработана.
    UPD1. Спасибо Cheater, подсказал, что std::shared_mutex появляется только в С++17std::shared_lock только в С++14). Тем, для кого это критично, видимо, придётся обойтись std::mutex.
    UPD2. Далее про потокобезопасность (без сохранения последовательности повествования).

    Проблема видимости события


    При использовании события в качестве члена класса кажется логичным сделать его видимость public, чтобы сторонние объекты могли добавлять/удалять свои обработчики. Однако это приведёт к тому, что operator(), т.е. вызов события, тоже будет доступен извне, что в ряде случаев может быть неприемлемо. Решим эту проблему выделением из класса события (TEvent<...>) абстрактного интерфейса, предназначенного только для оперирования обработчиками.

    template<class ...TParams>
    class IEvent
    {
        protected:
            using TEventHandler = AbstractEventHandler<TParams...>;
        public:
            bool operator+=( TEventHandler& eventHandler )
            {
               return addHandler( eventHandler );
            }
            bool operator-=( TEventHandler& eventHandler )
            {
                return removeHandler( eventHandler );
            }
        protected:
            IEvent() {}
            virtual bool addHandler( TEventHandler& eventHandler ) = 0;
            virtual bool removeHandler( TEventHandler& eventHandler ) = 0;
    };
    

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

    Теперь мы можем разнести в разные области видимости часть события, отвечающая за работу с обработчиками, и часть, отвечающую за его вызов.

    class TestWindow
    {
        . . .
        public:
            TestWindow() :
                onButtonClick( m_onButtonClick ),
                m_onButtonClick()
            {
            }
            IEvent<const std::string&, unsigned int>& onButtonClick;
        protected:
            TEvent<const std::string&, unsigned int> m_onButtonClick;
        . . .
    };
    

    Таким образом, теперь сторонние объекты могут добавлять/удалять свои обработчики через TestWindow::onButtonClick, однако не смогут сами вызвать это событие. Вызов теперь может осуществляться только внутри класса TestWindow (и его потомков, если область видимости события, как примере, protected).

    Тривиальный код понемногу начинает превращаться в нечто монструозное, но это ещё не конец.

    Соответствие параметров события и его обработчиков


    В текущей реализации событие и любой её обработчик должны иметь строго соответствующий список параметров. Это приводит к ряду недостатков.

    Первое. Предположим, у нас есть шаблон класса, в котором есть событие с шаблонным параметром.

    template<class TSource>
    class MyClass
    {
        . . .
        public:
            TEvent<const TSource&> onValueChanged;
        . . .
    };
    

    Ввиду того, что тип, который будет здесь использован, заранее неизвестен, есть смысл передавать его именно по константной ссылке, а не по значению. Однако теперь для любой имплементации, даже с фундаментальными типами, обязаны быть соответствующие обработчики.

    MyClass<bool> myBoolClass;
    . . .
    template<class TSource>
    class MyHandlerClass
    {
        . . .
        private:
            void handleValueChanged1( const bool& newValue );
            void handleValueChanged2( bool newValue );
        . . .
    };
    . . .
    MyHandlerClass myHandlerClass;
    myBoolClass.onValueChanged += METHOD_HANDLER( myHandlerClass, MyHandlerClass::handleValueChanged1 );  // OK
    myBoolClass.onValueChanged += METHOD_HANDLER( myHandlerClass, MyHandlerClass::handleValueChanged2 );  // compile error
    

    Хотелось бы иметь возможность соединять с подобным событием и обработчики вида MyHandlerClass::handleValueChanged2, но пока такой возможности нет.

    Второе. Попробуем реализовать обработчик-функтор аналогично уже имеющемуся обработчику-методу (-функции-члену класса).

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

    Теперь попробуем привинтить его к какому-нибудь событию.

    class TestWindow
    {
        . . .
        public:
            TEvent<const std::string&, unsigned int> onButtonClick;
        . . .
    };
    struct ClickEventHandler
    {
        void operator()( const std::string&, unsigned int ) { . . . }
    };
    int main( int argc, char *argv[] )
    {
        . . .
        TestWindow testWindow;
        ClickEventHandler clickEventHandler;
    
        testWindow.onButtonClick += FUNCTOR_HANDLER( clickEventHandler );
        . . .
    }
    

    Результатом будет ошибка компиляции. Для функции createFunctorEventHandler компилятор не может вывести типы TParams... из единственного аргумента этой функции — непосредственно функтора. Функтор действительно не содержит никакой информации о том, обработчик какого типа нужно создать на его основе. Единственное, что можно сделать в данной ситуации — это написать что-то вроде:

    testWindow.onButtonClick += createFunctorEventHandler<ClickEventHandler, const std::string&, unsigned int>( clickEventHandler );
    

    Но ведь делать этого совсем не хочется.

    Соединение события с обработчиками разных типов


    Итак, хотелка есть, дело за реализацией. Будем рассматривать ситуацию на примере обработчика-функтора, обработчик-метод (-функция-член класса) получится аналогичным образом.

    Раз уж на основе одного лишь функтора нельзя сказать, какой будет список параметров соответствующего ему обработчика, то и не станем этого делать. Актуальным этот вопрос становится не в момент создания обработчика, а в момент попытки присоединения его к конкретному событию. И да, это два разных момента. Реализовать эту идею можно следующим образом:

    template<class TFunctor> class FunctorHolder;
    
    template<class TFunctor, class ...TParams>
    class FunctorEventHandler : public AbstractEventHandler<TParams...>
    {
        public:
            FunctorEventHandler( FunctorHolder<TFunctor>& functorHolder ) :
                AbstractEventHandler<TParams...>(),
                m_functorHolder( functorHolder )
            {
            }
            virtual void call( TParams... params ) override
            {
                m_functorHolder.m_functor( params... );
            }    
        private:
            FunctorHolder<TFunctor>& m_functorHolder;
        . . .
    };
    

    template<class TFunctor>
    class FunctorHolder
    {
        public:
            FunctorHolder( TFunctor& functor ) :
                m_functor( functor )
            {
            }
            template<class ...TCallParams>
            operator AbstractEventHandler<TCallParams...>&()
            {
                return *new FunctorEventHandler<TFunctor, TCallParams...>( *this );
            }
        private:
            TFunctor& m_functor;
        . . .
        template<class TFunctor, class ...TParams> friend class FunctorEventHandler;
    };
    

    template<class TFunctor>
    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>
    class IEvent
    {
        protected:
            using TEventHandler = AbstractEventHandler<TParams...>;
        public:
            template<class TSome>
            bool operator+=( TSome&& some )
            {
               return addHandler( static_cast<TEventHandler&>( some ) );
            }
            template<class TSome>
            bool operator-=( TSome&& some )
            {
                return removeHandler( static_cast<TEventHandler&>( some ) );
            }
        protected:
            IEvent() {}
            virtual bool addHandler( TEventHandler& eventHandler ) = 0;
            virtual bool removeHandler( TEventHandler& eventHandler ) = 0;
    };
    

    Если вкратце, разделение моментов создания обработчика и присоединения его к событию здесь имеет более ярко выраженный характер, чем ранее. Это позволяет обойти проблемы, описанные в предыдущем пункте. Проверка совместимости типов произойдёт при попытке прикастовать определённый FunctorHolder к определённому FunctorEventHandler, а точнее, создать экземпляр класса FunctorEventHandler<.. .> с весьма конкретным типом функтора; и в этом классе будет строчка кода m_functorHolder.m_functor( params… );, которая просто не скомпилируется для набора типов, несовместимых с функтором (либо если это вообще не функтор, т.е. объект, не имеющий operator()).

    Повторюсь, что проблема удаления временных объектов будет рассмотрена ниже. Кроме того, стоит отметить, что куча макросов под каждый случай сделана, во-первых, с целью демонстрации возможностей данного типа обработчиков, а во-вторых, на случай возможной доработки напильником какого-либо из них.

    Проверим результат.

    class TestWindow
    {
        . . .
        public:
            TEvent<const std::string&, unsigned int> onButtonClick;
        . . .
    };
    struct Functor
    {
        void operator()( const std::string&, unsigned int ) {}
    };
    struct Functor2
    {
        void operator()( std::string, unsigned int ) {}
    };
    struct Functor3
    {
        void operator()( const std::string&, const unsigned int& ) {}
    };
    struct Functor4
    {
        void operator()( std::string, const unsigned int& ) {}
    };
    struct Functor5
    {
        void operator()( std::string&, unsigned int& ) {}
    };
    struct Functor6
    {
        void operator()( const std::string&, unsigned int& ) {}
    };
    struct Functor7
    {
        void operator()( std::string&, const unsigned int& ) {}
    };
    int main( 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
        . . .
    }
    

    Ошибка компиляции возникает при попытке преобразовать один из параметров из const lvalue в lvalue. Преобразование из rvalue в unconst lvalue ошибки не вызывает, хотя, стоит отметить, и создаёт потенциальную угрозу самовыстрела в ногу: обработчик будет иметь возможность изменять скопированную на стек переменную, которая радостно удалится при выходе из этого обработчика.

    Вообще, сообщение об ошибке должно выглядеть примерно следующим образом:

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

    Для большей наглядности при использовании событий и обработчиков в стороннем коде можно добавить ещё собственное сообщение об ошибке. Это потребует написание небольшой вспомогательной структуры (признаюсь, подобный подход я где-то подсмотрел):

    namespace
    {
        template<class TFunctor, class ...TParams>
        struct IsFunctorParamsCompatible
        {
            private:
                template<class TCheckedFunctor, class ...TCheckedParams>
                static constexpr std::true_type exists( decltype( std::declval<TCheckedFunctor>()( std::declval<TCheckedParams>()... ) )* = nullptr );
                
                template<class TCheckedFunctor, class ...TCheckedParams>
                static constexpr std::false_type exists( ... );
            public:
                static constexpr bool value = decltype( exists<TFunctor, TParams...>( nullptr ) )::value;
        };
    } //
    

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

    Работа этот дела основана на механизме SFINAE. Вкратце, происходит попытка компиляции первой функции exists, однако, если это не получается по причине несовместимости аргументов (либо отсутствия operator() у того, что передано в качестве функтора), компилятор не выкидывает ошибку, а просто пытается скомпилировать вторую функцию; мы делаем всё, чтобы её компиляция проходила успешно всегда, а затем по факту того, какая же из функций была скомпилирована, делаем вывод (записывая результат в value) о совместимости аргументов для заданных типов.

    Теперь сообщение об ошибке будет выглядеть примерно так:

    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 &'
    

    Кроме дополнительного более информативного сообщения об ошибке данный подход решает проблему преобразования аргумента(ов) из rvalue в unconst lvalue: теперь оно вызывает ошибку несовместимости аргументов, т.е. попытка добавления обработчика functor6 из примера выше приводит к ошибке времени компиляции.
    UPD. Доработка (без сохранения последовательности повествования).

    Сравнение функторов


    Ввиду изменений в классе-обработчике, немного изменится и реализация сравнения экземпляров этого класса. Вновь приведу реализацию только обработчика-функтора, потому что обработчик-метод (-функция-член класса) будет выглядеть аналогично.

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

    template<class TFunctor, class ...TParams>
    class FunctorEventHandler : public AbstractEventHandler<TParams...>
    {
        . . .
        using MyType = FunctorEventHandler<TFunctor, TParams...>;
        protected:
            virtual bool isEquals( 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<class TFunctor>
    class FunctorHolder
    {
        . . .
        using MyType = FunctorHolder<TFunctor>;
        public:
            bool operator==( const MyType& other ) const
            {
                return ( m_functor == other.m_functor );
            }
            bool operator!=( const MyType& other ) const
            {
                return !( *this == other );
            }
        private:
            TFunctor& m_functor;
        . . .
    };
    

    На этом сходства в реализации сравнения заканчиваются и начинается часть только для обработчиков-функторов.

    Как отмечалось выше, у нас получилось несколько типов обработчиков-функторов: непосредственно объекты-функторы, лямбда-выражения, экземпляры класса std::function, отдельные функции. Из них объекты-функторы, лямбда-выражения и экземпляры класса std::function не могут сравниваться с использованием operator== (их нужно сравнивать по адресу), а вот отдельные функции могут, т.к. уже хранятся по адресу. Чтобы не переписывать функцию сравнения отдельно для каждого случая, запишем её в общем виде:

    namespace
    {
        template<class TEqu, class TEnabled = void>
        struct EqualityChecker;
    
        template<class TEquatable>
        struct EqualityChecker<TEquatable, typename std::enable_if<is_equatable<TEquatable>::value>::type>
        {
            static constexpr bool isEquals( const TEquatable& operand1, const TEquatable& operand2 )
            {
                return ( operand1 == operand2 );
            }
        };
    
        template<class TNonEquatable>
        struct EqualityChecker<TNonEquatable, typename std::enable_if<!is_equatable<TNonEquatable>::value>::type>
        {
            static constexpr bool isEquals( const TNonEquatable& operand1, const TNonEquatable& operand2 )
            {
                return ( &operand1 == &operand2 );
            }
        };
    } //
    template<class TFunctor>
    class FunctorHolder
    {
        . . .
        using MyType = FunctorHolder<TFunctor>;
        public:
            bool operator==( const MyType& other ) const
            {
                return EqualityChecker<TFunctor>::isEquals( m_functor, other.m_functor );
            }
        private:
            TFunctor& m_functor;
        . . .
    };
    

    Подразумевается, что is_equatable — вспомогательный шаблон, определяющий могут ли быть проверены на равенство два экземпляра заданного типа. С его помощью, используя std::enable_if, мы выбираем одну из двух частично специализированных структур EqualityChecker, которая и будет проводить сравнение: по значению или по адресу. Реализован is_equatable он может быть следующим образом:

    template<class T>
    class is_equatable
    {
        private:
            template<class U>
            static constexpr std::true_type exists( decltype( std::declval<U>() == std::declval<U>() )* = nullptr );
    
            template<class U>
            static constexpr std::false_type exists( ... );
        public:
            static constexpr bool value = decltype( exists<T>( nullptr ) )::value;
    };
    

    Данная реализация основана на механизме SFINAE, который уже применялся ранее. Только здесь мы проверяем наличие operator== для экземпляров заданного класса.

    Таким вот нехитрым образом реализация сравнения обработчиков-функторов готова.

    Сборка мусора


    Будьте снисходительны, захотелось и мне вставить громкий заголовок.

    Приближаемся к финалу, и пора уже избавляться от огромного количества создаваемых объектов, которые никто не контролирует.

    При каждом действии события с обработчиком создаётся два объекта: Holder, хранящий исполняемую часть обработчика, и EventHandler, связывающий его с событием. Не забудем, что в случае попытки повторного добавления обработчика никакого добавления не произойдёт — два объекта «повисли в воздухе» (если, конечно, отдельно не проверять этот случай каждый раз). Другая ситуация: удаления обработчика; так же создаются два новых объекта для поиска такого же (равного) в списке обработчиков события; найденный обработчик из списка, конечно, удаляется (если есть), а этот временный, созданный для поиска и состоящий из двух объектов — опять «в воздухе». В общем, не круто.

    Обратимся к умным указателям. Нужно определить, какова будет семантика владения каждого из двух объектов обработчика: единоличное владение (std::unique_ptr) или разделяемое (std::shared_ptr).

    Holder, кроме использования самим событием при добавлении/удалении должен храниться в EventHandler'е, поэтому используем для разделяемое владение, а для EventHandler'а — единоличное, т.к. после создания он будет храниться только в списке обработчиков события.

    Реализуем эту идею:

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

    namespace
    {
        template<class TSome>
        struct HandlerCast
        {
            template<class ...Types>
            static constexpr THandlerPtr<Types...> cast( TSome& some )
            {
                return static_cast<THandlerPtr<Types...>>( some );
            }
        };
        template<class TPtr>
        struct HandlerCast<std::shared_ptr<TPtr>>
        {
            template<class ...Types>
            static constexpr THandlerPtr<Types...> cast( std::shared_ptr<TPtr> some )
            {
                return HandlerCast<TPtr>::cast<Types...>( *some );
            }
        };
    } //
    template<class ...TParams>
    class IEvent
    {
        public:
            template<class TSome>
            bool operator+=( TSome&& some )
            {
               return addHandler( HandlerCast<TSome>::cast<TParams...>( some ) );
            }
            template<class TSome>
            bool operator-=( TSome&& some )
            {
                return removeHandler( HandlerCast<TSome>::cast<TParams...>( some ) );
            }
        protected:
            using TEventHandlerPtr = THandlerPtr<TParams...>;
            IEvent() {}
            virtual bool addHandler( TEventHandlerPtr eventHandler ) = 0;
            virtual bool removeHandler( TEventHandlerPtr eventHandler ) = 0;
    };
    template<class ...TParams>
    class TEvent : public IEvent<TParams...>
    {
        using TEventHandlerIt = typename std::list<TEventHandlerPtr>::const_iterator;
        public:
    
            TEvent()
            {
                . . .
            }
            ~TEvent()
            {
                // empty
            }
        protected:
            virtual bool addHandler( 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 ) );
                    return true;
                }
                return false;
            }
            virtual bool removeHandler( TEventHandlerPtr eventHandler ) override
            {
                . . .
            }
        private:
            // использовать под залоченным для чтения 'm_handlerListMutex'
            inline TEventHandlerIt findEventHandler( const TEventHandlerPtr& eventHandler ) const
            {
                return std::find_if( m_handlers.cbegin(), m_handlers.cend(), [ &eventHandler ]( const TEventHandlerPtr& oneHandler )
                {
                    return ( *oneHandler == *eventHandler );
                } );
            }
            // использовать под залоченным для записи 'm_handlerListMutex'
            inline void deleteHandler( TEventHandlerIt it )
            {
                m_handlers.erase( it );
            }
            std::list<TEventHandlerPtr> m_handlers;
        . . .
    };
    

    template<class TMethodHolder, class ...TParams>
    class MethodEventHandler : 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<class TObject, class ...TParams>
    class MethodHolder
    {
        using MyType = MethodHolder<TObject, TParams...>;
        using TMethod = void( TObject::* )( TParams... );
        public:
            MethodHolder( TObject& object, TMethod method )
            {
                . . .
            }
            template<class ...TCallParams>
            operator THandlerPtr<TCallParams...>()
            {
                return THandlerPtr<TCallParams...>( new MethodEventHandler<MyType, TCallParams...>( /* ЧТО СЮДА ПЕРЕДАТЬ? */ ) );
            }
        . . .
    };
    template<class TObject, class ...TParams>
    std::shared_ptr<MethodHolder<TObject, TParams...>> createMethodEventHandler( TObject& object, void( TObject::*method )( TParams... ) )
    {
        return std::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 )
    

    Обо всём по порядку.

    Для начала событие и его интерфейс для работы с обработчиками. В последнем преобразовать типы непосредственным использованием static_cast больше не получится, потому что преобразуемый тип лежит «внутри» std::shared_ptr. Теперь для подобного преобразования будем использовать вспомогательную структуру HandlerCast, которая своей частной специализацией предоставит доступ к объекту внутри std::shared_ptr, а уже работая с ним (в своей неспециализированной реализации), применит старый добрый static_cast.

    Само событие; здесь тоже есть несколько важных изменений. Во-первых, перестанем вручную удалять экземпляры обработчиков в деструкторе и при удалении; теперь достаточно удалить из списка умный указатель с этим обработчиком. Кроме того, при добавлении обработчика важно не забыть std::move, т.к. std::unique_ptr не поддерживает копирование (что весьма логично для подобной семантики).

    Перейдём к обработчикам. По старой традиции приведён только один из них, второй аналогичен. И здесь, на первый взгляд, всё сводится к изменению типов хранимых/создаваемых объектов со ссылок/указателей на умные указатели.

    Но есть один тонкий момент. Функция createMethodEventHandler вернёт std::shared_ptr на экземпляр MethodHolder. Чуть позже произойдёт попытка преобразования его к типу обработчика (MethodEventHandler), где он должен будет создать новый экземпляр MethodEventHandler, передав ему в конструктор std::shared_ptr на себя. Именно так оно и задумывалось, чтобы экземпляр MethodHolder'а позже удалился при удалении экземпляра MethodEventHandler'а. Но проблема в том, что у MethodHolder'а нет доступа к уже созданному std::shared_ptr, хранящему его самого.

    Для решения проблему придётся хранить в MethodHolder'е умный указатель на себя же. Однако чтобы тот не влиял на его удаление, воспользуемся std::weak_ptr:

    template<class TObject, class ...TParams>
    class MethodHolder
    {
        using MyType = MethodHolder<TObject, TParams...>;
        using TMethod = void( TObject::* )( TParams... );
        public:
            template<class ...TCallParams>
            operator THandlerPtr<TCallParams...>()
            {
                return THandlerPtr<TCallParams...>( new MethodEventHandler<MyType, TCallParams...>( m_me.lock() ) );
            }
            template<class TObject, class ...TParams>
            static std::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<class TObject, class ...TParams>
    std::shared_ptr<MethodHolder<TObject, TParams...>> createMethodEventHandler( TObject& object, void( TObject::*method )( TParams... ) )
    {
        return MethodHolder<TObject, TParams...>::create( object, method );
    }
    

    Для большей понятности приведу примерный порядок событий при удалении обработчика из события (мои извинения за случайный каламбур):

    • событие удаляет элемент из списка (m_handlers.erase( it );), что приводит к вызову его деструктора;
    • вызывается деструктор std::unique_ptr, который приводит к вызову деструктора управляемого объекта;
    • вызывается деструктор MethodEventHandler, который удаляет все поля объекта, в том числе поле m_methodHolder, являющееся std::shared_ptr;
    • вызывается деструктор std::shared_ptr; он видит, что счётчик владельцев достиг нуля (т.к. на момент удаления из события он был единственным владельцем) и вызывает деструктор управляемого объекта (MethodHolder); однако уничтожение блока управления не вызывается, потому что счётчик ссылок std::weak_ptr пока не равен нулю;
    • вызывается деструктор MethodHolder, который приводит к уничтожению всех полей, в том числе, поля m_me, являющегося std::weak_ptr;
    • вызывается деструктор std::weak_ptr; его управляемый объект уже уничтожен; т.к. счётчик ссылок std::weak_ptr стал равным нулю, вызывается уничтожение блока управления;
    • профит.

    Важно помнить, что деструктор класса AbstractEventHandler должен быть виртуальным; иначе после пункта 2 в пункте 3 произойдёт вызов деструктора AbstractEventHandler и дальнейшие действия выполнены не будут.

    Соединение события и обработчика


    В ряде случаев, когда добавление/удаление одного обработчика из события происходит весьма часто (согласно какой-нибудь логике), не хочется возиться, доставая каждый раз экземпляр события и экземпляр обработчика, чтобы в очередной раз реализовать подписку/отписку от этого события. А хочется соединить их один раз, а затем по необходимости работать с этим соединением, добавляя/удаляя с его помощью заранее заданный обработчик из заранее заданного события. Реализовать это можно следующим образом:

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

    template<class ...TParams>
    class IEvent
    {
        . . .
        protected:
            using TEventHandlerPtr = THandlerPtr<TParams...>;
            virtual bool isHandlerAdded( const TEventHandlerPtr& eventHandler ) const = 0;
            virtual bool addHandler( TEventHandlerPtr eventHandler ) = 0;
            virtual bool removeHandler( TEventHandlerPtr eventHandler ) = 0;
        friend class HandlerEventJoin<TParams...>;
        . . .
    };
    template<class ...TParams>
    class TEvent : public IEvent<TParams...>
    {
        . . .
        protected:
            virtual bool isHandlerAdded( const TEventHandlerPtr& eventHandler ) const override
            {
                std::shared_lock<std::shared_mutex> _handlerListMutexLock( m_handlerListMutex );
                return ( findEventHandler( eventHandler ) != m_handlers.end() );
            }
            virtual bool addHandler( TEventHandlerPtr eventHandler ) override { . . . }
            virtual bool removeHandler( TEventHandlerPtr eventHandler ) override { . . . }
        private:
            // использовать под залоченным для чтения 'm_handlerListMutex'
            inline TEventHandlerIt findEventHandler( const TEventHandlerPtr& eventHandler ) const { . . . }
            
            std::list<TEventHandlerPtr> m_handlers;
            mutable std::shared_mutex m_handlerListMutex;
        . . .
    };
    

    template<class ...TParams>
    class HandlerEventJoin
    {
        public:
            HandlerEventJoin( IEvent<TParams...>& _event, THandlerPtr<TParams...> handler ) :
                m_event( _event ),
                m_handler( handler )
            {
            }
            inline bool isJoined() const
            {
                return m_event.isHandlerAdded( m_handler );
            }
            inline bool join()
            {
                return m_event.addHandler( m_handler );        
            }
            inline bool unjoin()
            {
                return m_event.removeHandler( m_handler );        
            }
        private:
            IEvent<TParams...>& m_event;
            THandlerPtr<TParams...> m_handler;
    };
    

    Как видно, теперь добавилось ещё одно возможное место хранения экземпляра обработчика, поэтому будем использовать для этого std::shared_ptr вместо std::unique_ptr.

    Однако данный класс, как по мне, слегка неудобен в использовании. Экземпляры соединений хотелось бы хранить и создавать без списка параметров, инстанцирующих шаблон класса.

    Реализуем это с помощью абстрактного класса-предка и обёртки:

    class AbstractEventJoin
    {
        public:
            virtual ~AbstractEventJoin() {}
    
            virtual bool isJoined() const = 0;
            virtual bool join() = 0;
            virtual bool unjoin() = 0;
        protected:
            AbstractEventJoin() {}
    };
    

    template<class ...TParams>
    class HandlerEventJoin : public AbstractEventJoin
    {
        . . .
        public:
            virtual inline bool isJoined() const override { . . . }
            virtual inline bool join() override { . . . }
            virtual inline bool unjoin() override { . . . }
        . . .
    };
    

    class EventJoinWrapper
    {
        public:
            template<class TSome, class ...TParams>
            inline EventJoinWrapper( IEvent<TParams...>& _event, TSome&& handler ) :
                m_eventJoin( std::make_shared<HandlerEventJoin<TParams...>>( _event, HandlerCast<TSome>::cast<TParams...>( handler ) ) )
            {
            }
            constexpr EventJoinWrapper() :
                m_eventJoin( nullptr )
            {
            }
            ~EventJoinWrapper()
            {
                if( m_eventJoin != nullptr )
                    delete m_eventJoin;
            }
            operator bool() const
            {
                return isJoined();
            }
            bool isAssigned() const
            {
                return ( m_eventJoin != nullptr );
            }
            bool isJoined() const
            {
                return ( m_eventJoin != nullptr && m_eventJoin->isJoined() );
            }
            bool join()
            {
                return ( m_eventJoin != nullptr ? m_eventJoin->join() : false );
            }
            bool unjoin()
            {
                return ( m_eventJoin != nullptr ? m_eventJoin->unjoin() : false );
            }
        private:
            AbstractEventJoin* m_eventJoin;
    };
    using EventJoin = EventJoinWrapper;
    

    HandlerCast — это та же вспомогательная структура, которая применялась здесь. Кстати, важно не забыть сделать деструктор AbstractEventJoin виртуальным, чтобы при удалении его экземпляра в деструкторе EventJoinWrapper вызвался деструктор HandlerEventJoin, иначе в последнем не уничтожится поле THandlerPtr и, следовательно, сам обработчик.

    Данная реализация кажется работоспособной, однако только на первый взгляд. Копирование или перемещение экземпляра EventJoinWrapper приведёт к повторному удалению m_eventJoin в его деструкторе. Поэтому используем std::shared_ptr для хранения экземпляра AbstractEventJoin, а также реализуем слегка оптимизированную семантику перемещения (и копирования), т.к. это будет потенциально частой операцией.

    class EventJoinWrapper
    {
        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;
    };
    

    Теперь можно при подключении обработчика к событию возвращать сразу экземпляр нового соединения:

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

    А после разруливания треугольной зависимости по include'ам (IEvent <= EventJointWrapper.hpp; EventJointWrapper <= HandlerEventJoin.hpp; HandlerEventJoin <= IEvent.hpp) разделением некоторых файлов на .h и .hpp с этим даже можно работать.

    Создание экземпляров соединений происходит по тем же правилам, что работают при подписке обработчика на событие:

    struct EventHolder
    {
        TEvent<const std::string&> onEvent;
    };
    struct MethodsHolder
    {
        void method1( const std::string& ) {}
        void method2( std::string ) {}
        void method3( std::string&& ) {}
        void method4( std::string& ) {}
        void method5( const int& ) {}
    };
    int main( 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 ) );  // error
        
        return 0;
    }
    

    Плюс, можно «включать»/«выключать» обработку события (для чего, в принципе, и создавались соединения):

    struct EventHolder
    {
        TEvent<const std::string&, unsigned int> onEvent;
    };
    struct MethodsHolder
    {
        void handleEvent( const std::string& text, unsigned int count )
        {
            std::cout << "Text '" << text << "' handled " << count << " times." << std::endl;
        }
    };
    int main__( int argc, char* argv[] )
    {
        EventHolder _eventHolder;
        MethodsHolder methodsHolder;
        EventJoin eventJoin = EventJoin( _eventHolder.onEvent, METHOD_HANDLER( methodsHolder, MethodsHolder::handleEvent ) );
    
        static const std::string handlingText = "testing...";
        for( int i = 0; i < 10; ++i )
        {
            if( eventJoin.isJoined() )
                eventJoin.unjoin();
            else
                eventJoin.join();
            _eventHolder.onEvent( handlingText, i );
        }
    
        return 0;
    }
    

    Text 'testing...' handled 0 times.
    Text 'testing...' handled 2 times.
    Text 'testing...' handled 4 times.
    Text 'testing...' handled 6 times.
    Text 'testing...' handled 8 times.
    

    Итог


    Сперва стоит отметить, что задача написать статью максимально коротко и сжато полностью провалена.

    Надеюсь, что полученная реализация обработки событий достаточно работоспособна и будет хоть кому-то полезна.

    Весьма громоздкий пример, демонстрирующий основные возможности
    #include <iostream>
    #include <functional>
    #include "events/event.hpp"
    #include "events/handler/methodeventhandler.hpp"
    #include "events/handler/functoreventhandler.hpp"
    #include "events/join/handlereventjoin.hpp"
    #include "events/join/eventjoinwrapper.hpp"
    
    class Foo
    {
        public:
            Foo() :
                onMake( m_onMake ),
                m_onMake(),
                m_onMakeInner(),
                m_makeCount( 0 )
            {
                m_onMakeInner += FUNCTOR_HANDLER( m_onMake );
            }
            IEvent<unsigned int>& onMake;
            void make()
            {
                m_onMakeInner( m_makeCount++ );
            }
        private:
            TEvent<unsigned int> m_onMake, m_onMakeInner;
            unsigned int m_makeCount;
    };
    namespace instances
    {
        Foo& getFoo()
        {
            static Foo _foo;
            return _foo;
        }
    } // instances
    
    struct FunctorHandler
    {
        void operator()( unsigned int makeCount );
    };
    void functionHandler( unsigned int makeCount );
    class ClassHandler
    {
        public:
            void handle( unsigned int makeCount );
    };
    namespace instances
    {
        FunctorHandler& getFunctorHandler()
        {
            static FunctorHandler _functorHandler;
            return _functorHandler;
        }
        std::function<void( unsigned int )>& getStdFunctionHandler()
        {
            static std::function<void( unsigned int )> _stdFunctionHandler = []( unsigned int makeCount )
            {
                std::cout << "It's std::function handler" << std::endl;
                if( makeCount >= 2 )
                    instances::getFoo().onMake -= STD_FUNCTION_HANDLER( instances::getStdFunctionHandler() );
            };
            return _stdFunctionHandler;
        }
        ClassHandler& getClassHandler()
        {
            static ClassHandler _classHandler;
            return _classHandler;
        }
    } // instances
    void FunctorHandler::operator()( unsigned int makeCount )
    {
        std::cout << "It's functor handler" << std::endl;
        if( makeCount >= 0 )
            instances::getFoo().onMake -= FUNCTOR_HANDLER( instances::getFunctorHandler() );
    }
    void functionHandler( unsigned int makeCount )
    {
        std::cout << "It's function handler" << std::endl;
        if( makeCount >= 3 )
            instances::getFoo().onMake -= FUNCTION_HANDLER( functionHandler );
    }
    void ClassHandler::handle( unsigned int makeCount )
    {
        std::cout << "It's method handler" << std::endl;
        if( makeCount >= 4 )
            instances::getFoo().onMake -= MY_METHOD_HANDLER( ClassHandler::handle );
    }
    
    int main( int argc, char* argv[] )
    {
        Foo& foo = instances::getFoo();
    
        auto lambdaHandler = []( unsigned int )
        {
            std::cout << "It's lambda handler" << std::endl;
        };
    
        foo.onMake += FUNCTOR_HANDLER( instances::getFunctorHandler() );
        foo.onMake += LAMBDA_HANDLER( lambdaHandler );
        EventJoin lambdaJoin = foo.onMake += LAMBDA_HANDLER( ( [ &foo, &lambdaHandler ]( unsigned int makeCount )
        {
            if( makeCount >= 1 )
                foo.onMake -= LAMBDA_HANDLER( lambdaHandler );
        } ) );
        foo.onMake += STD_FUNCTION_HANDLER( instances::getStdFunctionHandler() );
        foo.onMake += FUNCTION_HANDLER( functionHandler );
        foo.onMake += METHOD_HANDLER( instances::getClassHandler(), ClassHandler::handle );
    
        for( int i = 0; i < 6; ++i )
        {
            std::cout << "Make " << i << " time:" << std::endl;
            foo.make();
            std::cout << std::endl;
        }
        
        lambdaJoin.unjoin();
       
        return 0;
    }
    

    Вывод:

    Make 0 time:
    It's functor handler
    It's lambda handler
    It's std::function handler
    It's function handler
    It's method handler
    
    Make 1 time:
    It's lambda handler
    It's std::function handler
    It's function handler
    It's method handler
    
    Make 2 time:
    It's std::function handler
    It's function handler
    It's method handler
    
    Make 3 time:
    It's function handler
    It's method handler
    
    Make 4 time:
    It's method handler
    
    Make 5 time:
    


    Стоит отметить ряд важных моментов:

    • не оговаривалось отдельно, поэтому упомяну, что само событие в данной реализации является функтором, а значит, может выступать в качестве обработчика другого события;
    • сейчас нельзя использовать в качестве обработчиков константные методы (функции-члены класса); думаю, если понадобится такая возможность, нетрудно написать для этого новый тип обработчика на основе уже имеющихся.

    Кроме того, в финальной версии присутствуют некоторые моменты, опущенные в статье для большей наглядности и читаемости кода:

    • тип возвращаемого значения метода (функции-члена класса) для соответствующего обработчика может быть любым, не обязательно void (для обработчиков-функторов это выполнялось и так);
    • вся реализация завёрнута в namespace'ы для удобства использования в проектах (если кому-то это кажется лишним, их всегда можно убрать);
    • кое-где добавлен спецификатор noexcept.

    Всем тем, кто дочитал досюда хотя бы по диагонали, низкий поклон. Прилагаю весь код; его можно взять ещё и здесь (со всеми последними доработками).

    Весь код
    ./events/helpers/is_equatable.hpp
    #pragma once
    
    #include <type_traits>
    
    
    template<class T>
    class is_equatable
    {
        private:
    
            template<class U>
            static constexpr std::true_type exists( decltype( std::declval<U>() == std::declval<U>() )* = nullptr ) noexcept;
    
            template<class U>
            static constexpr std::false_type exists( ... ) noexcept;
    
        public:
    
            static constexpr bool value = decltype( exists<T>( nullptr ) )::value;
    };
    


    ./events/handlers/abstracteventhandler.hpp
    #pragma once
    
    #include "eventhandlerptr.h"
    
    
    namespace events {
    
    namespace handlers {
    
    
    template<class ...TParams>
    class AbstractEventHandler
    {
        using MyType = AbstractEventHandler<TParams...>;
    
        public:
    
            virtual ~AbstractEventHandler() {}
    
            virtual void call( TParams... params ) = 0;
    
            bool operator==( const MyType& other ) const noexcept
            {
                return isEquals( other );
            }
            bool operator!=( const MyType& other ) const noexcept
            {
                return !( *this == other );
            }
    
        protected:
        
            AbstractEventHandler() {}
    
            virtual bool isEquals( const MyType& other ) const noexcept = 0;
    };
    
    
    } // handlers
    
    } // events
    


    ./events/handlers/eventhandlerptr.h
    #pragma once
    
    #include <memory>
    
    
    namespace events {
    
    namespace handlers {
    
    
    template<class ...TParams> class AbstractEventHandler;
    
    
    template<class ...Types>
    using TEventHandlerPtr = std::shared_ptr<AbstractEventHandler<Types...>>;
    
    
    } // handlers
    
    } // events
    


    ./events/handlers/functoreventhandler.hpp
    #pragma once
    
    #include <memory>
    #include <assert.h>
    #include "abstracteventhandler.hpp"
    #include "../helpers/is_equatable.hpp"
    
    
    namespace events {
    
    namespace handlers {
    
    
    namespace
    {
    
        template<class TFunctor, class ...TParams>
        struct IsFunctorParamsCompatible
        {
            private:
    
                template<class TCheckedFunctor, class ...TCheckedParams>
                static constexpr std::true_type exists( decltype( std::declval<TCheckedFunctor>()( std::declval<TCheckedParams>()... ) )* = nullptr ) noexcept;
                
                template<class TCheckedFunctor, class ...TCheckedParams>
                static constexpr std::false_type exists( ... ) noexcept;
    
            public:
    
                static constexpr bool value = decltype( exists<TFunctor, TParams...>( nullptr ) )::value;
        };
    
    } //
    
    
    template<class TFunctor> class FunctorHolder;
    
    template<class TFunctor, class ...TParams>
    class FunctorEventHandler : public AbstractEventHandler<TParams...>
    {
        using MyType = FunctorEventHandler<TFunctor, TParams...>;
        using TFunctorHolderPtr = std::shared_ptr<FunctorHolder<TFunctor>>;
    
        public:
    
            FunctorEventHandler( TFunctorHolderPtr functorHolder ) :
                AbstractEventHandler<TParams...>(),
                m_functorHolder( functorHolder )
            {
                assert( m_functorHolder != nullptr );
            }
    
            virtual void call( TParams... params ) override
            {
                static_assert( IsFunctorParamsCompatible<TFunctor, TParams...>::value, "Event and functor arguments are not compatible" );
    
                m_functorHolder->m_functor( params... );
            }
    
        protected:
    
            virtual bool isEquals( const AbstractEventHandler<TParams...>& other ) const noexcept override 
            {
                const MyType* _other = dynamic_cast<const MyType*>( &other );
                return ( _other != nullptr && *m_functorHolder == *_other->m_functorHolder );
            }
    
        private:
    
            TFunctorHolderPtr m_functorHolder;
    };
    
    
    namespace
    {
    
        template<class TEqu, class TEnabled = void>
        struct EqualityChecker;
    
        template<class TEquatable>
        struct EqualityChecker<TEquatable, typename std::enable_if<is_equatable<TEquatable>::value>::type>
        {
            static constexpr bool isEquals( const TEquatable& operand1, const TEquatable& operand2 ) noexcept
            {
                return ( operand1 == operand2 );
            }
        };
    
        template<class TNonEquatable>
        struct EqualityChecker<TNonEquatable, typename std::enable_if<!is_equatable<TNonEquatable>::value>::type>
        {
            static constexpr bool isEquals( const TNonEquatable& operand1, const TNonEquatable& operand2 ) noexcept
            {
                return ( &operand1 == &operand2 );
            }
        };
    
    } //
    
    template<class TFunctor>
    class FunctorHolder
    {
        using MyType = FunctorHolder<TFunctor>;
    
        public:
    
            template<class ...TCallParams>
            operator TEventHandlerPtr<TCallParams...>()
            {
                return TEventHandlerPtr<TCallParams...>( new FunctorEventHandler<TFunctor, TCallParams...>( m_me.lock() ) );
            }
    
            bool operator==( const MyType& other ) const noexcept
            {
                return EqualityChecker<TFunctor>::isEquals( m_functor, other.m_functor );
            }
            bool operator!=( const MyType& other ) const noexcept
            {
                return !( *this == other );
            }
    
            template<class TFunctor>
            static std::shared_ptr<MyType> create( TFunctor&& functor )
            {
                std::shared_ptr<MyType> result( new MyType( functor ) );
                result->m_me = result;
                return result;
            }
    
        private:
    
            FunctorHolder( TFunctor& functor ) :
                m_functor( functor ),
                m_me()
            {
            }
    
            TFunctor& m_functor;
    
            std::weak_ptr<MyType> m_me;
    
        template<class TFunctor, class ...TParams> friend class FunctorEventHandler;
    };
    
    
    template<class TFunctor>
    std::shared_ptr<FunctorHolder<TFunctor>> createFunctorEventHandler( TFunctor&& functor )
    {
        return FunctorHolder<TFunctor>::create( functor );
    }
    
    
    } // handlers
    
    } // events
    
    
    #define     FUNCTOR_HANDLER( Functor )              ::events::handlers::createFunctorEventHandler( Functor )
    #define     LAMBDA_HANDLER( Lambda )                FUNCTOR_HANDLER( Lambda )
    #define     STD_FUNCTION_HANDLER( StdFunction )     FUNCTOR_HANDLER( StdFunction )
    #define     FUNCTION_HANDLER( Function )            FUNCTOR_HANDLER( &Function )
    


    ./events/handlers/methodeventhandler.hpp
    #pragma once
    
    #include <memory>
    #include <assert.h>
    #include "abstracteventhandler.hpp"
    
    
    namespace events {
    
    namespace handlers {
    
    
    namespace
    {
    
        template<class TMethodHolder, class ...TParams>
        struct IsMethodParamsCompatible
        {
            private:
    
                template<class TCheckedMethodHolder, class ...TCheckedParams>
                static constexpr std::true_type exists( decltype( ( std::declval<TCheckedMethodHolder>().m_object.*std::declval<TCheckedMethodHolder>().m_method )( std::declval<TCheckedParams>()... ) )* = nullptr ) noexcept;
                
                template<class TCheckedMethodHolder, class ...TCheckedParams>
                static constexpr std::false_type exists( ... ) noexcept;
    
            public:
    
                static constexpr bool value = decltype( exists<TMethodHolder, TParams...>( nullptr ) )::value;
        };
    
    } //
    
    
    template<class TMethodHolder, class ...TParams>
    class MethodEventHandler : public AbstractEventHandler<TParams...>
    {
        using MyType = MethodEventHandler<TMethodHolder, TParams...>;
        using TMethodHolderPtr = std::shared_ptr<TMethodHolder>;
    
        public:
    
            MethodEventHandler( TMethodHolderPtr methodHolder ) :
                AbstractEventHandler<TParams...>(),
                m_methodHolder( methodHolder )
            {
                assert( m_methodHolder != nullptr );
            }
    
            virtual void call( TParams... params ) override
            {
                static_assert( IsMethodParamsCompatible<TMethodHolder, TParams...>::value, "Event and method arguments are not compatible" );
    
                ( m_methodHolder->m_object.*m_methodHolder->m_method )( params... );
            }
    
        protected:
    
            virtual bool isEquals( const AbstractEventHandler<TParams...>& other ) const noexcept override
            {
                const MyType* _other = dynamic_cast<const MyType*>( &other );
                return ( _other != nullptr && *m_methodHolder == *_other->m_methodHolder );
            }
    
        private:
    
            TMethodHolderPtr m_methodHolder;
    };
    
    
    template<class TObject, class TResult, class ...TParams>
    class MethodHolder
    {
        using MyType = MethodHolder<TObject, TResult, TParams...>;
        using TMethod = TResult( TObject::* )( TParams... );
    
        public:
    
            template<class ...TCallParams>
            operator TEventHandlerPtr<TCallParams...>()
            {
                return TEventHandlerPtr<TCallParams...>( new MethodEventHandler<MyType, TCallParams...>( m_me.lock() ) );
            }
    
            bool operator==( const MyType& other ) const noexcept
            {
                return ( &m_object == &other.m_object && m_method == other.m_method );
            }
            bool operator!=( const MyType& other ) const noexcept
            {
                return !( *this == other );
            }
    
            template<class TObject, class ...TParams>
            static std::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<class TMethodHolder, class ...TParams> friend class MethodEventHandler;
        template<class TMethodHolder, class ...TParams> friend struct IsMethodParamsCompatible;
    };
    
    
    template<class TObject, class TResult, class ...TParams>
    std::shared_ptr<MethodHolder<TObject, TResult, TParams...>> createMethodEventHandler( TObject& object, TResult( TObject::*method )( TParams... ) )
    {
        return MethodHolder<TObject, TResult, TParams...>::create( object, method );
    }
    
    
    } // handlers
    
    } // events
    
    
    #define     METHOD_HANDLER( Object, Method )     ::events::handlers::createMethodEventHandler( Object, &Method )
    #define     MY_METHOD_HANDLER( Method )          METHOD_HANDLER( *this, Method )
    


    ./events/handlers/handlercast.hpp
    #pragma once
    
    #include <memory>
    #include "eventhandlerptr.h"
    
    
    namespace events {
    
    namespace handlers {
    
    
    template<class TSome>
    struct HandlerCast
    {
        template<class ...Types>
        static constexpr TEventHandlerPtr<Types...> cast( TSome& some )
        {
            return static_cast<TEventHandlerPtr<Types...>>( some );
        }
    };
    
    template<class TPtr>
    struct HandlerCast<std::shared_ptr<TPtr>>
    {
        template<class ...Types>
        static constexpr TEventHandlerPtr<Types...> cast( std::shared_ptr<TPtr> some )
        {
            return HandlerCast<TPtr>::cast<Types...>( *some );
        }
    };
    
    
    } // handlers
    
    } // events
    


    ./events/event.hpp
    #pragma once
    
    #include <type_traits>
    #include <list>
    #include <memory>
    #include <shared_mutex>
    #include <algorithm>
    #include <assert.h>
    #include "handlers/abstracteventhandler.hpp"
    #include "handlers/eventhandlerptr.h"
    #include "handlers/handlercast.hpp"
    #include "joins/eventjoinwrapper.hpp"
    
    
    namespace events {
    
    
    namespace joins
    {
        template<class ...TParams> class HandlerEventJoin;
    }
    
    template<class ...TParams>
    class IEvent
    {
        public:
    
            template<class TSome>
            EventJoin operator+=( TSome&& some )
            {
                EventJoin result( *this, std::forward<TSome>( some ) );
                result.join();
                return result;
            }
    
            template<class TSome>
            bool operator-=( TSome&& some )
            {
                return removeHandler( handlers::HandlerCast<TSome>::cast<TParams...>( some ) );
            }
    
        protected:
    
            using TMyEventHandlerPtr = handlers::TEventHandlerPtr<TParams...>;
    
            IEvent() {}
    
            virtual bool isHandlerAdded( const TMyEventHandlerPtr& eventHandler ) const = 0;
            virtual bool addHandler( TMyEventHandlerPtr eventHandler ) = 0;
            virtual bool removeHandler( TMyEventHandlerPtr eventHandler ) = 0;
    
        friend class joins::HandlerEventJoin<TParams...>;
    };
    
    
    template<class ...TParams>
    class TEvent : public IEvent<TParams...>
    {
        using TEventHandlerIt = typename std::list<TMyEventHandlerPtr>::const_iterator;
    
        public:
    
            TEvent() :
                m_handlers(),
                m_currentIt(),
                m_isCurrentItRemoved( false ),
                m_handlerListMutex()
            {
            }
    
            void operator()( 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();
            }
    
        protected:
    
            virtual bool isHandlerAdded( const TMyEventHandlerPtr& eventHandler ) const override
            {
                std::shared_lock<std::shared_mutex> _handlerListMutexLock( m_handlerListMutex );
    
                return ( findEventHandler( eventHandler ) != m_handlers.end() );
    
            }
            virtual bool addHandler( TMyEventHandlerPtr eventHandler ) override
            {
                std::unique_lock<std::shared_mutex> _handlerListMutexLock( m_handlerListMutex );
    
                if( findEventHandler( eventHandler ) == m_handlers.end() )
                {
                    m_handlers.push_back( std::move( eventHandler ) );
                    return true;
                }
                return false;
            }
            virtual bool removeHandler( TMyEventHandlerPtr eventHandler ) override
            {
                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 );
    
                    return true;
                }
                return false;
            }
    
        private:
    
            // использовать под залоченным для чтения 'm_handlerListMutex'
            inline TEventHandlerIt findEventHandler( const TMyEventHandlerPtr& eventHandler ) const noexcept
            {
                return std::find_if( m_handlers.cbegin(), m_handlers.cend(), [ &eventHandler ]( const TMyEventHandlerPtr& oneHandler )
                {
                    return ( *oneHandler == *eventHandler );
                } );
            }
            // использовать под залоченным для записи 'm_handlerListMutex'
            inline void deleteHandler( TEventHandlerIt it )
            {
                m_handlers.erase( it );
            }
    
            std::list<TMyEventHandlerPtr> m_handlers;
    
            // использовать под залоченным 'm_handlerListMutex'
            mutable TEventHandlerIt m_currentIt;
            mutable bool m_isCurrentItRemoved;
    
            mutable std::shared_mutex m_handlerListMutex;
    };
    
    
    } // events
    


    ./events/joins/abstracteventjoin.h
    #pragma once
    
    
    namespace events {
    
    namespace joins {
    
    
    class AbstractEventJoin
    {
        public:
    
            virtual ~AbstractEventJoin();
    
            virtual bool isJoined() const = 0;
            virtual bool join() = 0;
            virtual bool unjoin() = 0;
    
        protected:
    
            AbstractEventJoin();
    };
    
    
    } // joins
    
    } // events
    


    ./events/joins/abstracteventjoin.cpp
    #include "abstracteventjoin.h"
    
    
    namespace events {
    
    namespace joins {
    
    
    AbstractEventJoin::AbstractEventJoin()
    {
    }
    
    AbstractEventJoin::~AbstractEventJoin()
    {
    }
    
    
    } // joins
    
    } // events
    


    ./events/joins/handlereventjoin.h
    #pragma once
    
    #include "abstracteventjoin.h"
    #include "../handlers/eventhandlerptr.h"
    
    
    namespace events {
    
    
    template<class ...TParams> class IEvent;
    
    
    namespace joins {
    
    
    template<class ...TParams>
    class HandlerEventJoin : public AbstractEventJoin
    {
        public:
    
            HandlerEventJoin( IEvent<TParams...>& _event, ::events::handlers::TEventHandlerPtr<TParams...> handler ) :
                AbstractEventJoin(),
                m_event( _event ),
                m_handler( handler )
            {
            }
    
            virtual inline bool isJoined() const override;
            virtual inline bool join() override;
            virtual inline bool unjoin() override;
    
        private:
    
            IEvent<TParams...>& m_event;
            ::events::handlers::TEventHandlerPtr<TParams...> m_handler;
    };
    
    
    } // joins
    
    } // events
    


    ./events/joins/handlereventjoin.hpp
    #pragma once
    
    #include "handlereventjoin.h"
    #include "../event.hpp"
    
    
    namespace events {
    
    namespace joins {
    
    
    template<class ...TParams>
    bool HandlerEventJoin<TParams...>::isJoined() const
    {
        return m_event.isHandlerAdded( m_handler );
    }
    
    template<class ...TParams>
    bool HandlerEventJoin<TParams...>::join()
    {
        return m_event.addHandler( m_handler );
    }
    
    template<class ...TParams>
    bool HandlerEventJoin<TParams...>::unjoin()
    {
        return m_event.removeHandler( m_handler );
    }
    
    
    } // joins
    
    } // events
    


    ./events/joins/eventjoinwrapper.h
    #pragma once
    
    #include <memory>
    #include "../handlers/eventhandlerptr.h"
    
    
    namespace events {
    
    
    template<class ...TParams> class IEvent;
    
    
    namespace joins {
    
    
    class AbstractEventJoin;
    
    
    class EventJoinWrapper
    {
        public:
    
            template<class TSome, class ...TParams>
            inline EventJoinWrapper( IEvent<TParams...>& _event, TSome&& handler );
    
            constexpr EventJoinWrapper() noexcept;
            EventJoinWrapper( EventJoinWrapper&& other ) noexcept;
            EventJoinWrapper( EventJoinWrapper& other ) noexcept;
    
            EventJoinWrapper& operator=( EventJoinWrapper&& other ) noexcept;
            EventJoinWrapper& operator=( const EventJoinWrapper& other ) noexcept;
    
            operator bool() const;
    
            bool isAssigned() const;
    
            bool isJoined() const;
            bool join();
            bool unjoin();
    
        private:
    
            std::shared_ptr<AbstractEventJoin> m_eventJoin;
    };
    
    
    } // joins
    
    
    using EventJoin = joins::EventJoinWrapper;
    
    
    } // events
    


    ./events/joins/eventjoinwrapper.hpp
    #pragma once
    
    #include "eventjoinwrapper.h"
    #include "handlereventjoin.h"
    #include "../handlers/handlercast.hpp"
    
    
    namespace events {
    
    namespace joins {
    
    
    template<class TSome, class ...TParams>
    EventJoinWrapper::EventJoinWrapper( IEvent<TParams...>& _event, TSome&& handler ) :
        m_eventJoin( std::make_shared<HandlerEventJoin<TParams...>>( _event, ::events::handlers::HandlerCast<TSome>::cast<TParams...>( handler ) ) )
    {
    }
    
    
    } // joins
    
    } // events
    


    ./events/joins/eventjoinwrapper.cpp
    #include "eventjoinwrapper.h"
    
    #include <type_traits>
    #include "abstracteventjoin.h"
    
    
    namespace events {
    
    namespace joins {
    
    
    constexpr EventJoinWrapper::EventJoinWrapper() noexcept :
        m_eventJoin( nullptr )
    {
    }
    
    EventJoinWrapper::EventJoinWrapper( EventJoinWrapper&& other ) noexcept :
        m_eventJoin( std::move( other.m_eventJoin ) )
    {
    }
    
    EventJoinWrapper::EventJoinWrapper( EventJoinWrapper& other ) noexcept :
        m_eventJoin( other.m_eventJoin )
    {
    }
    
    
    EventJoinWrapper& EventJoinWrapper::operator=( EventJoinWrapper&& other ) noexcept
    {
        m_eventJoin = std::move( other.m_eventJoin );
    
        return *this;
    }
    
    EventJoinWrapper& EventJoinWrapper::operator=( const EventJoinWrapper& other ) noexcept
    {
        m_eventJoin = other.m_eventJoin;
    
        return *this;
    }
    
    
    EventJoinWrapper::operator bool() const
    {
        return isJoined();
    }
    
    
    bool EventJoinWrapper::isAssigned() const
    {
        return ( m_eventJoin != nullptr );
    }
    
    
    bool EventJoinWrapper::isJoined() const
    {
        return ( m_eventJoin != nullptr && m_eventJoin->isJoined() );
    }
    
    bool EventJoinWrapper::join()
    {
        return ( m_eventJoin != nullptr ? m_eventJoin->join() : false );
    }
    
    bool EventJoinWrapper::unjoin()
    {
        return ( m_eventJoin != nullptr ? m_eventJoin->unjoin() : false );
    }
    
    
    } // joins
    
    } // events
    



    UPD1. Здесь и ранее по статье приведён код, написанный под VC++14. Для совместимости с другими компиляторами лучше брать код по ссылке. Отдельная благодарность Cheater за обеспечение совместимости с GCC.
    UPD2. Спасибо lexxmark, что заметил дыру в потокобезопасности в плане нескольких одновременных вызовов события.
    Небольшие доработки
    namespace
    {
        template<class ...TParams>
        struct TypeHelper
        {
            using TEventHandlerPtr = handlers::TEventHandlerPtr<TParams...>;
            using TEventHandlerIt = typename std::list<TEventHandlerPtr>::const_iterator;
        };
    } //
    template<class ...TParams>
    class IEvent
    {
        . . .
        protected:
            using TMyEventHandlerPtr = typename TypeHelper<TParams...>::TEventHandlerPtr;
        . . .
    };
    namespace
    {
        template<class ...TParams>
        struct EventCore
        {
            using TMyHandlerPtr = typename TypeHelper<TParams...>::TEventHandlerPtr;
            std::list<TMyHandlerPtr> handlers;
            mutable std::shared_mutex coreMutex;
        };
        template<class ...TParams>
        class HandlerRunner
        {
            using TMyEventCore = EventCore<TParams...>;
            using TMyHandlerIt = typename TypeHelper<TParams...>::TEventHandlerIt;
            public:
                HandlerRunner( TMyEventCore& eventCore ) :
                    m_eventCore( eventCore ),
                    currentIt(),
                    wasRemoving( false )
                {
                }
                void run( TParams... params )
                {
                    m_eventCore.coreMutex.lock_shared();
    
                    currentIt = m_eventCore.handlers.begin();
                    wasRemoving = false;
                    while( currentIt != m_eventCore.handlers.end() )
                    {
                        m_eventCore.coreMutex.unlock_shared();
                        ( *currentIt )->call( params... );
                        m_eventCore.coreMutex.lock_shared();
    
                        if( wasRemoving )
                            wasRemoving = false;
                        else
                            ++currentIt;
                    }
    
                    m_eventCore.coreMutex.unlock_shared();
                }
                TMyHandlerIt currentIt;
                mutable bool wasRemoving;
            private:
                TMyEventCore& m_eventCore;
        };
    } //
    template<class ...TParams>
    class TEvent : public IEvent<TParams...>
    {
        using TMyEventHandlerPtr = typename TypeHelper<TParams...>::TEventHandlerPtr;
        using TMyEventHandlerIt = typename TypeHelper<TParams...>::TEventHandlerIt;
        using TMyHandlerRunner = HandlerRunner<TParams...>;
        public:
            TEvent() :
                m_core()
            {
            }
            void operator()( TParams... params )
            {
                TMyHandlerRunner newHandlerRunner( m_core );
    
                m_core.coreMutex.lock_shared();
                auto it = m_handlerRunners.insert( m_handlerRunners.end(), &newHandlerRunner );
                m_core.coreMutex.unlock_shared();
    
                newHandlerRunner.run( params... );
    
                m_core.coreMutex.lock_shared();
                m_handlerRunners.erase( it );
                m_core.coreMutex.unlock_shared();
            }
        protected:
            virtual bool isHandlerAdded( const TMyEventHandlerPtr& eventHandler ) const override
            {
                std::shared_lock<std::shared_mutex> _coreMutexLock( m_core.coreMutex );
                return ( findEventHandler( eventHandler ) != m_core.handlers.end() );
            }
            virtual bool addHandler( TMyEventHandlerPtr eventHandler ) override
            {
                std::unique_lock<std::shared_mutex> _coreMutexLock( m_core.coreMutex );
    
                if( findEventHandler( eventHandler ) == m_core.handlers.end() )
                {
                    m_core.handlers.push_back( std::move( eventHandler ) );
                    return true;
                }
                return false;
            }
            virtual bool removeHandler( TMyEventHandlerPtr eventHandler ) override
            {
                std::unique_lock<std::shared_mutex> _coreMutexLock( m_core.coreMutex );
    
                auto it = findEventHandler( eventHandler );
                if( it != m_core.handlers.end() )
                {
                    for( TMyHandlerRunner* oneHandlerRunner : m_handlerRunners )
                    {
                        if( it == oneHandlerRunner->currentIt )
                        {
                            ++oneHandlerRunner->currentIt;
                            oneHandlerRunner->wasRemoving = true;
                        }
                    }
                    m_core.handlers.erase( it );
                    return true;
                }
                return false;
            }
        private:
            // использовать под залоченным для чтения 'm_core.coreMutex'
            inline TMyEventHandlerIt findEventHandler( const TMyEventHandlerPtr& eventHandler ) const
            {
                return std::find_if( m_core.handlers.cbegin(), m_core.handlers.cend(), [ &eventHandler ]( const TMyEventHandlerPtr& oneHandler )
                {
                    return ( *oneHandler == *eventHandler );
                } );
            }
            EventCore<TParams...> m_core;
            std::list<TMyHandlerRunner*> m_handlerRunners;
    };
    

    Здесь для обеспечения возможности одновременных вызовов события (точнее, нового вызова, пока ещё не завершились предыдущие) применяется вспомогательный класс HandlerRunner, который отвечает за один вызов всех обработчиков. Соответственно, он содержит рассмотренные ранее поля: currentIt (итератор текущего в переборе обработчика) и wasRemoving (определяющее, было ли совершено удаление текущего в переборе обработчика). Сами экземпляры HandlerRunner'ов хранятся в событии и живут в течение вызова его operator(); в процессе работы они обращаются к данным (например, списку обработчиков) самого события, используя вспомогательную структуру EventCore. Т.о. ситуация очень похожа на рассмотренную ранее, за исключением того, что текущий в переборе итератор и флаг того, был ли удалён текущий обработчик, теперь содержатся не в единственном экземпляре в самом событии, а создаются новые для каждого его вызова.
    UPD3. Благодаря isnullxbh найдена ещё одна ошибка. Она связана с неправильным сохранением и последующим обращением к объектам, переданным по rvalue (в основном, лямбда-выражениям).
    Исправление
    Для исправления будем сохранять объекты, переданные по lvalue, копированием lvalue-ссылки, а объекты, переданные по rvalue, копировать непосредственно (перемещать, если возможно). В этом нам поможет вспомогательная структура:
    template<class TSome>
    struct ObjectSaver;
    
    template<class LValue>
    struct ObjectSaver<LValue&>
    {
        using TObject = LValue&;
    };
    
    template<class RValue>
    struct ObjectSaver<RValue&&>
    {
        using TObject = RValue;
    };
    

    Чтобы Holder мог иметь различную реализацию в зависимости от переданного типа объекта (lvalue или rvalue), но не включал дополнительный тип в свой шаблон, нужно «отцепить» его от типа переданного объекта. Поможет нам в этом type erasing (неплохо описано в этой статье). Реализуем вспомогательный класс, который поможет Holder'у хранить переданный объект.
    template<class TBase>
    struct AbstractInnerHolder
    {
        virtual ~AbstractInnerHolder() {}
        virtual inline TBase& get() = 0;
        inline const TBase& get() const
        {
            return const_cast<AbstractInnerHolder<TBase>&>( *this ).get();
        }
    };
    template<class TBase, class TInner>
    struct TInnerHolder : public AbstractInnerHolder<TBase>
    {
        using TInnerObject = typename ObjectSaver<TInner>::TObject;
        TInnerHolder( TInner _inner ) :
            AbstractInnerHolder<TBase>(),
            inner( std::forward<TInner>( _inner ) )
        {
        }
        virtual inline TBase& get() override
        {
            return static_cast<TBase&>( inner );
        }
        TInnerObject inner;
    };
    template<class TAssignBase, class TArgInner>
    AbstractInnerHolder<TAssignBase>& createInnerHolder( TArgInner&& inner )
    {
        using TAssignInner = decltype( inner );
        return *new TInnerHolder<TAssignBase, TAssignInner>( std::forward<TArgInner>( inner ) );
    }
    

    Осталось сделать соответствующие изменения в Holder'ах. Покажу на примере MethodHolder'а.
    template<class TObject, class TResult, class ...TParams>
    class MethodHolder
    {
        . . .
        using MyType = MethodHolder<TObject, TResult, TParams...>;
        using TMethod = TResult( TObject::* )( TParams... );
        public:
            ~MethodHolder()
            {
                delete &m_innerHolder;
            }
            bool operator==( const MyType& other ) const
            {
                return ( &m_innerHolder.get() == &other.m_innerHolder.get() && m_method == other.m_method );
            }
            template<class TArgObject>
            static std::shared_ptr<MyType> create( TArgObject&& object, TMethod method )
            {
                std::shared_ptr<MyType> result( new MyType( std::forward<TArgObject>( object ), method ) );
                result->m_me = result;
                return result;
            }
        private:
    
            template<class TArgObject>
            MethodHolder( TArgObject&& object, TMethod method ) :
                m_innerHolder( createInnerHolder<TObject>( std::forward<TArgObject>( object ) ) ),
                m_method( method )
            {
                assert( m_method != nullptr );
            }
            AbstractInnerHolder<TObject>& m_innerHolder;
            TMethod m_method;
            std::weak_ptr<MyType> m_me;
        . . .
    };
    

    namespace
    {
        template<class TMethodHolder, class ...TParams>
        struct IsMethodParamsCompatible
        {
            private:
                template<class TCheckedMethodHolder, class ...TCheckedParams>
                static constexpr std::true_type exists( decltype( ( std::declval<TCheckedMethodHolder>().m_innerHolder.get().*std::declval<TCheckedMethodHolder>().m_method )( std::declval<TCheckedParams>()... ) )* = nullptr );
            . . .
        };
    } //
    template<class TMethodHolder, class ...TParams>
    class MethodEventHandler : public AbstractEventHandler<TParams...>
    {
        public:
            virtual void call( TParams... params ) override
            {
                static_assert( IsMethodParamsCompatible<TMethodHolder, TParams...>::value, "Event and method arguments are not compatible" );
                ( m_methodHolder->m_innerHolder.get().*m_methodHolder->m_method )( params... );
            }
        private:
            TMethodHolderPtr m_methodHolder;
        . . .
    };
    

    template<class TObject, class TResult, class ...TParams>
    std::shared_ptr<MethodHolder<typename std::decay<TObject>::type, TResult, TParams...>> createMethodEventHandler( TObject&& object, TResult( std::decay<TObject>::type::*method )( TParams... ) )
    {
        return MethodHolder<std::decay<TObject>::type, TResult, TParams...>::create( std::forward<TObject>( object ), method );
    }
    

    Готово. FunctorHolder реализуется по аналогии. Теперь все объекты можно передавать созданными на стеке. И такое применение лямбда-выражений больше не вызывает ошибок.

    P.S. Сравнение с механизмом сигналов/слотов Qt


    Думаю, не ошибусь, если скажу что Qt является весьма распространённым фреймворком для разработки на C++. Кроме всего прочего, есть в нём и свой механизм обработки событий, в котором есть сигналы как аналоги событий и слоты как аналоги обработчиков. Реализуется он с помощью Meta-Object Compiler'а, являющегося частью более глобальной Meta-Object System, которая, в свою очередь, реализована с помощью используемой в Qt надстройки над С++.

    Особенности обеих реализаций:


    • возможность подключения сигналов (событий) к методам (функциям-членам), функторам и функциям;
    • возможность подключения к сигналу (событию) слотов (обработчиков), типы аргументов которых могут отличаться видом ссылки (lvalue, rvalue);
    • возможность подключения сигнала к сигналу (события к событию);
    • возможность создания соединения (подключения) при подписке обработчика на событие (подключении сигнала к слоту).

    Преимущества Qt:


    • возможность уменьшения количества аргументов;
      в Qt предусмотрена возможность подключения к сигналам слотов с меньшим количеством аргументов; редуцироваться аргументы могут только «справа», иными словами, сигнатура слота должна соответствовать «левой части» сигнатуры сигнала; как частный случай, к любому сигналу можно подключить слот без аргументов; пожалуй, это может быть удобно в ряде случаев;
    • возможность создания нескольких одинаковых подключений;
      по умолчанию Qt не ограничивает возможность несколько раз подключить к одному сигналу (одного и того же объекта) один и тот же слот (одного и того же объекта); такое ограничение можно задавать вручную при каждом подключении, используя Qt::UniqueConnection; хотя мне субъективно и не нравится, какое поведение выбрано в качестве поведения по умолчанию, стоит признать, что Qt всё же даёт возможность выбора в этом вопросе;
    • возможность исполнения слота в потоке объекта, которому он принадлежит;
      задаётся такое поведение использованием Qt::QueuedConnection или Qt::BlockingQueuedConnection при подключении слота к сигналу; в этом случае код слота (обработчика) будет исполнен в потоке владельца этого слота; иначе он был бы исполнен в потоке владельца сигнала (события); на практике это означает, что обработчик будет вызван не мгновенно при срабатывании события, а в следующей итерации цикла обработки событий нужного потока; т.е. это бывает полезно, даже когда владелец сигнала и владелец слота находятся в одном потоке; правда, аргументы в этом случае специальным образом копируются.

    Недостатки Qt:


    • обязательное наследование от QObject;
      это кажется незначительным недостатком, однако, наследуясь от QObject, мы перенимаем все его ограничения, в частности, невозможность виртуального наследования (отсюда: Virtual inheritance with QObject is not supported.); в ряде случаев это, на мой взгляд, серьёзный недостаток;
    • невозможность использования в template'ах;
      в шаблоне класса нельзя использовать сигналы и слоты, даже если он является public-наследником QObject; это ещё одно ограничение moc'а; конечно, его всегда можно
      обойти,
      #include <QObject>
      class AbstractProperty : public QObject
      {
          Q_OBJECT
          protected:
              AbstractProperty();
          signals:
              void valueChanged();
      };
      template<class TSource>
      class TProperty : public AbstractProperty
      {
          public:
              TProperty( const TSource& value = TSource() ) :
                  AbstractProperty(),
                  m_value( value )
              {
              }
              const TSource& value() const
              {
                  return m_value;
              }
              void setValue( const TSource& newValue )
              {
                  if( newValue != m_value )
                  {
                      m_value = newValue;
                      emit valueChanged();
                  }
              }
          private:        
              TSource m_value;
      };
      

      К сожалению, сигнал valueChanged не передаёт нового (а по желанию, и старого) значения свойства, и в обработчике приходится обращаться к нему напрямую.
      но, на мой взгляд, это менее удобно в использовании;
    • невозможность нахождения декларации класса в .cpp-файле;
      подробнее;
    • невозможность подключения по экземпляру класса QMetaObject::Connection;
      как уже отмечалось выше, в Qt функция подключения (соединения сигнала и слота) тоже возвращает некий объект, отвечающий за это подключение; с его помощью можно отключить (отсоединить) слот от сигнала, не задумываясь о том, какой это слот какого объекта и какой это сигнал какого объекта; однако с его помощью нельзя подключить слот к сигналу настолько же просто, как и создать его заранее на основе сигнала и слота; в Qt он создаётся только функцией подключения;
    • использование дополнительно генерируемого moc'ом кода;
      это уже совсем субъективно, но решение, где на каждый класс, использующий сигналы и слоты (слоты не всегда) приходится несколько (по файлу на каждую конфигурацию) сгенерированных файлов доставляет некоторые неудобства; но скажем честно, это самый незначительный недостаток.

    Важно отметить, что данное сравнение с Qt всё же весьма субъективно и не нацелено на превознесение или осуждение данного фреймворка. Нужно помнить, что кроме механизма сигналов/слотов Qt предоставляет большой функционал, как использующий этот механизм, так и не зависящий от него. В любом случае, решать, что использовать, а что нет, всегда вам.

    Комментарии 35

      0

      В Qt есть The Event System и Signals & Slots. Почему для сравнения берутся сигналы из Qt, а не сигналы из Boost?

        0
        В Qt есть The Event System и Signals & Slots.

        Данная реализация как раз представляет собой нечто близкое к 'Signals & Slots', а не к 'The Event System'; последнее ближе к очереди (циклу) событий, там объекты взаимодействуют через цикл событий, а не напрямую. На мой взгляд, это всё же другой подход.
        Почему для сравнения берутся сигналы из Qt, а не сигналы из Boost?

        Вообще не было цели сравнивать хоть с чем-то. Это, скорее, просто бонус, вызванный тем, что с сигналами/слотами Qt мне приходилось работать (а с сигналами из boost нет).
          0
          В контексте GUI ожидается граф сцены, в ивенте содержатся координаты мыши, а ивент проходится по графу сцены.

          Тогда очевидный вопрос зачем? Когда есть множество реализаций тех же сингалов?
            0
            В контексте GUI ожидается граф сцены, в ивенте содержатся координаты мыши, а ивент проходится по графу сцены.

            Как я уже сказал, я не считаю этот подход равнозначным. Как Вы верно заметили, даже в Qt реализованы они оба (и The Event System, и Signals & Slots). Кроме того, use case'ов много (не только лишь GUI), не везде удобно одно и то же.
            Тогда очевидный вопрос зачем? Когда есть множество реализаций тех же сингалов?

            Согласен. Как минимум, когда не хочется/можется подключать библиотеку с очередной реализаций сигналов. Возможно, кому-то будет удобно и полезно. Плюс, небольшая демонстрация разных возможностей С++ (но это тоже скорее доп).
        0
        Дочитав до потокобезопасности возник вопрос. Зачем?
          +1
          Так, вроде бы, там написано, зачем. Вы с чем-то из этого не согласны?
          +1
          невозможность нахождения декларации класса в .cpp-файле;
          Это неправда: у меня декларация находится в cpp-файле — moc генерируется! Всё работает! Можете убрать из недостатков ;)
            0
            А у Вас класс, описанный в .cpp, имеет директиву Q_OBJECT (и соответственно, её применение в виде, например, сигналов)?
              0
              Конечно имеет Q_OBJECT.
              0
              А какая у Вас версия Qt? Перепроверил — всё подтвердилось. Я сейчас использую Qt5.6.1-1 (надо бы, конечно, обновиться...). Компилятор — VS++14 (соответственно, и MSBuild в качестве сборщика).
              Ошибка на этапе сборке.
              При описанном в cpp-файле классе с директивой Q_OBJECT выдаётся предупреждение
              Warning MSB8017 A circular dependency has been detected while executing custom build commands for item "GeneratedFiles\Debug\filename.moc". This may cause incremental build to work incorrectly.
              и все методы, которые должны быть сгенерированы moc'ом (metaObject, qt_metacast и т.д.), становятся unresolved, вызывая соответствующие ошибки. Кроме того в Generated Files, где под каждую конфигурацию должен присутствовать сгенерированный файлик moc_filename.cpp, находится только несуществующий filename.moc.
              Возможно, дело именно в MSBuild'е или его взаимодействии с Qt. Если это и вправду так, нужно будет действительно убрать этот пункт из недостатков.
                +1
                Обычно решение — это включить filename.moc в конце файла filename.cpp, файл должен генерироваться при сборке.
                  0
                  Это действительно решило проблему сборку (хоть предупреждение сборщика и осталось).
                  Спасибо; недостаток убираю.
                  0
                  Qt 5.11.1, GCC 8.2.1, Linux
                +1
                C++11 это вы преуменьшаете :) std::shared_mutex ----> C++17.

                Пара поверхностных замечаний (толком код не смотрел):
                — Не собралось под GCC из-за некоторого несоответствия стандарту (например в шаблонном производном классе нельзя использовать unqualified type name из шаблонного базового класса — name lookup не обязан туда заглядывать). См пулл-реквест;
                — У меня впечатление, что shared_ptr для Holder не обязателен и я бы попытался заменить его на unique_ptr в списке обработчиков + сырой readonly указатель во всех остальных местах.
                — shared_ptr(new XXX) -----> лучше использовать std::make_shared(XXX) (классика)
                — Некоторые внутренние типы лучше спрятать из публичного интерфейса (eg. IsMethodParamsCompatible)
                  0
                  C++11 это вы преуменьшаете :) std::shared_mutex ----> C++17.
                  Действительно, размахнулся что-то.
                  Не собралось под GCC из-за некоторого несоответствия стандарту
                  Это всё моя привычка разработки под VC. Надо будет поисправлять.
                  У меня впечатление, что shared_ptr для Holder не обязателен и я бы попытался заменить его на unique_ptr в списке обработчиков + сырой readonly указатель во всех остальных местах.
                  Немного не понял. В списке обработчиков как раз не Holder, а EventHandler, в котором уже Holder. Вы всё-таки при EventHandler?
                  лучше использовать std::make_shared
                  там, где применяется не он, используется private конструктор (в Holder'ах, например, это сделано для правильной инициализации поля m_me).
                  Некоторые внутренние типы лучше спрятать из публичного интерфейса
                  так, вроде убраны же в анонимные namespace'ы.
                  А вообще, за замечания спасибо)
                    0
                    Вы всё-таки при EventHandler?

                    Упс, да, EventHandler конечно.
                    так, вроде убраны же в анонимные namespace'ы.

                    Но они экспортируются в публичный заголовочный файл eventhandling.hpp. Такие вещи лучше прятать в cpp файлы.
                      0
                      Такие вещи лучше прятать в cpp файлы.
                      А как это сделать в случае с шаблонами? Вы можете привести небольшой пример?
                        0

                        С шаблонными классами это невозможно (виноват, проглядел <> в IsMethodParamsCompatible), кроме ест-но переноса IsMethodParamsCompatible в cpp файл с некрасивым захардкоживанием специализаций в *.cpp файле:


                        // methodeventhandler.hpp
                        template<class TMethodHolder, class ...TParams>
                        class MethodEventHandler : public AbstractEventHandler<TParams...>
                        {
                                //...
                                virtual void call( TParams... params ) override; // прячем определение в cpp файл
                               // ...
                        };

                        // methodeventhandler.cpp
                        template<class TMethodHolder, class ... TParams>
                        struct IsMethodParamsCompatible {
                        // определение класса...
                        };
                        
                        template<class TMethodHolder, class ...TParams>
                        void MethodEventHandler<TMethodHolder, TParams...>::call(TParams...params)
                        {
                        // тело ф-и, скопированное из hpp
                            static_assert( IsMethodParamsCompatible<TMethodHolder, TParams...>::value, "Event and method arguments are not compatible" );
                        
                            ( m_methodHolder->m_object.*m_methodHolder->m_method )( params... );
                        }
                        
                        // ХАРДКОД (принудительное инстанцирование заранее известного типа).
                        // Без кода ниже будет ошибка линковки при сборке test.cpp -
                        // unresolved symbol MethodEventHandler<MethodHolder<ClassHandler, int, unsigned int>, unsigned int>::call(unsigned int)
                        class ClassHandler;
                        
                        MethodEventHandler<events::handlers::MethodHolder<ClassHandler, int, unsigned int>, unsigned int> test(nullptr);
                        
                    0
                    Спасибо за поддержку совместимости с GCC!
                    0
                    Возможно я не внимательно читал, но зачем это всё написано? В Forms .Net C# это есть, в MFC/ATL тоже проблем с обработчиками событий нет. Или это всё ради кроссплатформенности и Qt?
                      0
                      Не очень понятно, при чём здесь C# (которым я, кстати, во многом вдохновлялся), если в нём, в отличие от C++, данный механизм встроен в язык. Насчёт «ради Qt» тоже не очень понятно, потому что данная реализация позволяет как раз не использовать сторонние библиотеки и фреймворки (по крайней мере, только ради сигналов).
                        0
                        Мы в своём проекте завезли свои сигналы без зависимостей, дело то не хитрое.
                      +3
                      Блин да очевидно зачем, полноценный легковесный механизм эвентов позволяющий реализовывать высокоуровневую бизнес логику без привязки к какому-то стороннему фреймворку же.
                      За показ хода мысли отдельное спасибо автору.
                        +2
                        Мне, в свое время, понравилась статья Потокобезопасные сигналы, которыми действительно удобно пользоваться

                        Потенциально, вызовы трёх возможных функций — добавления, удаления и перебора (при срабатывании события) обработчиков — возможны из разных потоков в случайные моменты времени. Это создаёт целое поле возможностей по их «пересечению» во времени, «накладыванию» их исполнения друг на друга и падению программы в итоге. Попробуем избежать этого; мьютексы — наше всё.

                        Честно говоря не вижу как решена проблема с параллельным выполнением «перебора». Или вызовом «перебора» из какого-то обработчика. Похоже что такие ситуации никак не накрыты.

                        Иными словами оператор () не потокобезопасный и неентерабельный. Ну и нет нет никаких гарантий с точки зрения исключений в обработчиках.

                               void operator()( 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();
                                }
                        
                          0
                          Спасибо за важное замечание, кое-что подправил (в частности, параллельный вызов события).
                          Ну и нет нет никаких гарантий с точки зрения исключений в обработчиках
                          А вот тут не совсем понятно, почему событие должно обрабатывать исключения в клиентском коде (если можно так назвать обработчики).
                            +1
                            А вот тут не совсем понятно, почему событие должно обрабатывать исключения в клиентском коде (если можно так назвать обработчики).


                            Обрабатывать чужие исключения конечно не нужно. Вопрос, в каком состоянии будет евент, если произойдет исключение в обработчике. Про гаратнии можно почитать здесь Exception_safety. Стандартные контейнеры вроде в большинстве случаев имеют Strong exception guarantee. Для класса общего пользования иметь Basic exception guarantee вполне неплохо.

                                    void operator()( TParams... params )
                                    {
                                        TMyHandlerRunner newHandlerRunner( m_core );
                            
                                        m_core.coreMutex.lock_shared();
                                        auto it = m_handlerRunners.insert( m_handlerRunners.end(), &newHandlerRunner );
                                        m_core.coreMutex.unlock_shared();
                                         //
                                        // если исключение бросится здесь
                                        //
                                        newHandlerRunner.run( params... );
                                        //
                                        // то вот это все не отработает и в m_handlerRunners застрянет указатель на локальный newHandlerRunner 
                                        //
                                        m_core.coreMutex.lock_shared();
                                        m_handlerRunners.erase( it );
                                        m_core.coreMutex.unlock_shared();
                                    }
                            

                            Ну и удалять обработчик, если он сейчас работает нельзя. Если это функтор и в середине его работы происходит отписка и он дальше выполняет какой-то код, то мы в момент отписки удалим функтор и весь остальной код фунтора будет работать на удаленном объекте. Здесь надо помечать обрабртчик на удаление, а удалять в функции рассылки (при условии что он не рассылается в паралельном цикле рассылки)

                            Вообще в реализациях евентов есть подводные камни, которые в стандартных реализациях решены или документированы (в большинстве своем).

                            Хотя мы используем самописный евент, который за годы эксплуатации не раз подправляли.
                              0
                              Большое спасибо за разъяснение.
                              Явно есть, куда двигаться дальше.
                          0
                          А как Вы сравниваете слоты? Можете механизм сравнения описать словами?
                            0
                            Этому был посвящён целый пункт.
                            Но если вкратце: функторы сравниваются по адресу объекта, методы (функции-члены) — по адресу экземпляра класса (которому они принадлежат) и своему адресу.
                            Естественно, сравниваются только однотипные объекты: как функторы, так и методы классов. Разнотипные обработчики считаются неравными.
                              0
                              Т.е., я не смогу использовать ф-ию или лямбду в качестве слота?
                                0
                                Наоборот, сможете. И объект-функтор, и лямбда-выражение, и экземпляр std::function, и отдельную функцию (ну, и метод класса/структуры, конечно).
                                  0
                                  Звучит неплохо :) Еще один вопрос, извините, что надоедаю. Как Вы сравниваете два слота, созданных на основе лямбда-функций?
                                    0
                                    Все объекты сравниваются по значению, если это возможно; в противном случае происходит сравнение по адресу. Конкретно лямбды сравниваются по адресу.
                            0
                            Признайтесь, вдохновение в дотнетовских делегатах не черпали?
                              +1
                              Признаю)

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое