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

habr.png

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

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


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

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

template
class TEvent
{
    using TEventHandler = AbstractEventHandler;
    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 m_handlers;
};

template
class MethodEventHandler : public AbstractEventHandler
{
    using TMethod = void( TObject::* )( TParams... );
    public:
        MethodEventHandler( TObject& object, TMethod method ) :
            AbstractEventHandler(),
            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
AbstractEventHandler& createMethodEventHandler( TObject& object, void( TObject::*method )( TParams... ) )
{
    return *new MethodEventHandler( object, method );
}

#define METHOD_HANDLER( Object, Method ) createMethodEventHandler( Object, &Method )
#define MY_METHOD_HANDLER( Method ) METHOD_HANDLER( *this, Method )


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

class TestWindow
{
    . . .
    public:
        TEvent 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 AbstractEventHandler
{
    . . .
    using MyType = AbstractEventHandler;
    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 MethodEventHandler : public AbstractEventHandler
{
    . . .
    using TMethod = void( TObject::* )( TParams... );
    protected:
        virtual bool isEquals( const AbstractEventHandler& other ) const override
        {
            const MyType* _other = dynamic_cast( &other );
            return ( _other != nullptr && &m_object == &_other.m_object && m_method == _other.m_method );
        }
    private:
        TObject& m_object;
        TMethod m_method;
    . . .
};


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

template
class TEvent
{
    . . .
    using TEventHandler = AbstractEventHandler;
    using TEventHandlerIt = typename std::list::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 m_handlers;
    . . .
};


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

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

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


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

class TestWindow
{
    . . .
    public:
        TEvent 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 TEvent
{
    using TEventHandler = AbstractEventHandler;
    using TEventHandlerIt = typename std::list::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 _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 _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 m_handlers;

        // использовать под залоченным 'm_handlerListMutex'
        mutable TEventHandlerIt m_currentIt;
        mutable bool m_isCurrentItRemoved;

        mutable std::shared_mutex m_handlerListMutex;
};


Не забудем оставлять «окно» незалоченночти при вызове каждого обработчика. Это нужно затем, чтобы внутри обработчика можно было обращаться к событию и изменять его (например, добавлять/удалять обработчики), не вызывая deadlock. За валидность данных можно не опасаться, потому что, как мы выяснили, единственное, что к этому приводит — это удаление текущего элемента, а данная ситуация обработана.
При использовании события в качестве члена класса кажется логичным сделать его видимость public, чтобы сторонние объекты могли добавлять/удалять свои обработчики. Однако это приведёт к тому, что operator (), т.е. вызов события, тоже будет доступен извне, что в ряде случаев может быть неприемлемо. Решим эту проблему выделением из класса события (TEvent<...>) абстрактного интерфейса, предназначенного только для оперирования обработчиками.

template
class IEvent
{
    protected:
        using TEventHandler = AbstractEventHandler;
    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 TEvent : public IEvent
{
    . . .
    public:
        TEvent() :
            IEvent()
            . . .
        {
        }
    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& onButtonClick;
    protected:
        TEvent m_onButtonClick;
    . . .
};


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

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


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

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

template
class MyClass
{
    . . .
    public:
        TEvent onValueChanged;
    . . .
};


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

MyClass myBoolClass;
. . .
template
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 FunctorEventHandler : public AbstractEventHandler
{
    public:
        FunctorEventHandler( TFunctor& functor ) :
            AbstractEventHandler(),
            m_functor( functor )
        {
        }
        virtual void call( TParams... params ) override final
        {
            m_functor( params... );
        }
    private:
        TFunctor& m_functor;
};

template
AbstractEventHandler& createFunctorEventHandler( TFunctor&& functor )
{
    return *new FunctorEventHandler( functor );
}

#define FUNCTOR_HANDLER( Functor ) createFunctorEventHandler( Functor )


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

class TestWindow
{
    . . .
    public:
        TEvent 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 );


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

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

template class FunctorHolder;

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

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

template
FunctorHolder& createFunctorEventHandler( TFunctor&& functor )
{
    return *new FunctorHolder( 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 IEvent
{
    protected:
        using TEventHandler = AbstractEventHandler;
    public:
        template
        bool operator+=( TSome&& some )
        {
           return addHandler( static_cast( some ) );
        }
        template
        bool operator-=( TSome&& some )
        {
            return removeHandler( static_cast( 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 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
    struct IsFunctorParamsCompatible
    {
        private:
            template
            static constexpr std::true_type exists( decltype( std::declval()( std::declval()... ) )* = nullptr );
            
            template
            static constexpr std::false_type exists( ... );
        public:
            static constexpr bool value = decltype( exists( nullptr ) )::value;
    };
} //

template
class FunctorEventHandler : public AbstractEventHandler
{
    . . .
    public:
        virtual void call( TParams... params ) override
        {
            static_assert( IsFunctorParamsCompatible::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 из примера выше приводит к ошибке времени компиляции.
Ввиду изменений в классе-обработчике, немного изменится и реализация сравнения экземпляров этого класса. Вновь приведу реализацию только обработчика-функтора, потому что обработчик-метод (-функция-член класса) будет выглядеть аналогично.

template
class AbstractEventHandler
{
    . . .
    using MyType = AbstractEventHandler;
    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 FunctorEventHandler : public AbstractEventHandler
{
    . . .
    using MyType = FunctorEventHandler;
    protected:
        virtual bool isEquals( const AbstractEventHandler& other ) const override
        {
            const MyType* _other = dynamic_cast( &other );
            return ( _other != nullptr && *m_functorHolder == *_other->m_functorHolder );
        }
    private:
        FunctorHolder& m_functorHolder;
    . . .
};

template
class FunctorHolder
{
    . . .
    using MyType = FunctorHolder;
    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
    struct EqualityChecker;

    template
    struct EqualityChecker::value>::type>
    {
        static constexpr bool isEquals( const TEquatable& operand1, const TEquatable& operand2 )
        {
            return ( operand1 == operand2 );
        }
    };

    template
    struct EqualityChecker::value>::type>
    {
        static constexpr bool isEquals( const TNonEquatable& operand1, const TNonEquatable& operand2 )
        {
            return ( &operand1 == &operand2 );
        }
    };
} //
template
class FunctorHolder
{
    . . .
    using MyType = FunctorHolder;
    public:
        bool operator==( const MyType& other ) const
        {
            return EqualityChecker::isEquals( m_functor, other.m_functor );
        }
    private:
        TFunctor& m_functor;
    . . .
};


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

template
class is_equatable
{
    private:
        template
        static constexpr std::true_type exists( decltype( std::declval() == std::declval() )* = nullptr );

        template
        static constexpr std::false_type exists( ... );
    public:
        static constexpr bool value = decltype( exists( nullptr ) )::value;
};


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

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


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

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

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

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

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

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

template
class AbstractEventHandler
{
    . . .
    public:
        virtual ~AbstractEventHandler() {}
    . . .
};

template
using THandlerPtr = std::unique_ptr>;

namespace
{
    template
    struct HandlerCast
    {
        template
        static constexpr THandlerPtr cast( TSome& some )
        {
            return static_cast>( some );
        }
    };
    template
    struct HandlerCast>
    {
        template
        static constexpr THandlerPtr cast( std::shared_ptr some )
        {
            return HandlerCast::cast( *some );
        }
    };
} //
template
class IEvent
{
    public:
        template
        bool operator+=( TSome&& some )
        {
           return addHandler( HandlerCast::cast( some ) );
        }
        template
        bool operator-=( TSome&& some )
        {
            return removeHandler( HandlerCast::cast( some ) );
        }
    protected:
        using TEventHandlerPtr = THandlerPtr;
        IEvent() {}
        virtual bool addHandler( TEventHandlerPtr eventHandler ) = 0;
        virtual bool removeHandler( TEventHandlerPtr eventHandler ) = 0;
};
template
class TEvent : public IEvent
{
    using TEventHandlerIt = typename std::list::const_iterator;
    public:

        TEvent()
        {
            . . .
        }
        ~TEvent()
        {
            // empty
        }
    protected:
        virtual bool addHandler( TEventHandlerPtr eventHandler ) override
        {
            std::unique_lock _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 m_handlers;
    . . .
};

template
class MethodEventHandler : public AbstractEventHandler
{
    . . .
    using TMethodHolderPtr = std::shared_ptr;
    public:
        MethodEventHandler( TMethodHolderPtr methodHolder ) :
            AbstractEventHandler(),
            m_methodHolder( methodHolder )
        {
            assert( m_methodHolder != nullptr );
        }
    private:
        TMethodHolderPtr m_methodHolder;
    . . .
};
template
class MethodHolder
{
    using MyType = MethodHolder;
    using TMethod = void( TObject::* )( TParams... );
    public:
        MethodHolder( TObject& object, TMethod method )
        {
            . . .
        }
        template
        operator THandlerPtr()
        {
            return THandlerPtr( new MethodEventHandler( /* ЧТО СЮДА ПЕРЕДАТЬ? */ ) );
        }
    . . .
};
template
std::shared_ptr> createMethodEventHandler( TObject& object, void( TObject::*method )( TParams... ) )
{
    return std::shared_ptr>( new MethodHolder( 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 MethodHolder
{
    using MyType = MethodHolder;
    using TMethod = void( TObject::* )( TParams... );
    public:
        template
        operator THandlerPtr()
        {
            return THandlerPtr( new MethodEventHandler( m_me.lock() ) );
        }
        template
        static std::shared_ptr create( TObject& object, TMethod method )
        {
            std::shared_ptr 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 m_me;
};
template
std::shared_ptr> createMethodEventHandler( TObject& object, void( TObject::*method )( TParams... ) )
{
    return MethodHolder::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
using THandlerPtr = std::shared_ptr>;

template
class IEvent
{
    . . .
    protected:
        using TEventHandlerPtr = THandlerPtr;
        virtual bool isHandlerAdded( const TEventHandlerPtr& eventHandler ) const = 0;
        virtual bool addHandler( TEventHandlerPtr eventHandler ) = 0;
        virtual bool removeHandler( TEventHandlerPtr eventHandler ) = 0;
    friend class HandlerEventJoin;
    . . .
};
template
class TEvent : public IEvent
{
    . . .
    protected:
        virtual bool isHandlerAdded( const TEventHandlerPtr& eventHandler ) const override
        {
            std::shared_lock _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 m_handlers;
        mutable std::shared_mutex m_handlerListMutex;
    . . .
};

template
class HandlerEventJoin
{
    public:
        HandlerEventJoin( IEvent& _event, THandlerPtr 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& m_event;
        THandlerPtr 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 HandlerEventJoin : public AbstractEventJoin
{
    . . .
    public:
        virtual inline bool isJoined() const override { . . . }
        virtual inline bool join() override { . . . }
        virtual inline bool unjoin() override { . . . }
    . . .
};

class EventJoinWrapper
{
    public:
        template
        inline EventJoinWrapper( IEvent& _event, TSome&& handler ) :
            m_eventJoin( std::make_shared>( _event, HandlerCast::cast( 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::
    
            

© Habrahabr.ru