Eu acho que o processamento de eventos como uma forma de interação entre objetos no OOP é conhecido por quase todo mundo que já tocou no OOP pelo menos uma vez. Pelo menos, essa abordagem é muito conveniente em uma gama muito ampla, na minha opinião, de tarefas. Em muitas linguagens de programação, o mecanismo de manipulação de eventos é incorporado; no entanto, em C ++ não existe esse mecanismo. Vamos ver o que você pode fazer sobre isso.
Breve introdução
Um evento é algo que pode acontecer com algum objeto sob certas condições (por exemplo, com um botão quando você clica nele com o mouse). Outras entidades podem precisar estar cientes disso; então eles se
inscrevem no evento . Nesse caso, quando um evento ocorre, o
manipulador de um objeto de terceiros inscrito no evento é chamado; Assim, ele tem a oportunidade de executar algum código, ou seja, responder a um evento. Da mesma forma, um objeto pode
cancelar a inscrição de um evento se não quiser mais responder a ele. Como resultado, temos muitos objetos que podem ser conectados usando os eventos de um deles e a reação a esses eventos de outros.
Algo assim, embora todos saibam disso.
Implementação mais simples
Parece fácil implementar esse comportamento. E pode ficar assim:
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 )
A aplicação deste caso deve ter a seguinte forma:
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 ); . . . }
Naturalmente, um método manipulador (membro da função de uma classe) não será o único tipo de manipulador, mas mais sobre isso posteriormente.
Tudo parece conveniente, compacto e ótimo. Mas, embora haja várias deficiências.
Comparação de manipuladores
Para implementar a desinscrição de um evento, você precisa adicionar a capacidade de comparar com o manipulador (por
== e
! == ). Os manipuladores que invocam o mesmo método (uma função membro de uma classe) do mesmo objeto (ou seja, a mesma instância da mesma classe) serão considerados iguais.
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; . . . };
Em seguida, poderemos remover manipuladores da assinatura do evento. Nesse caso, é necessário proibir a adição dos mesmos manipuladores (iguais).
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; . . . };
Aqui, as funções de adição / remoção do manipulador retornam
verdadeiro se for bem-sucedido e
falso se a ação correspondente (adicionar ou remover) não tiver sido executada.
Sim, o caso de uso com comparação envolve a criação de manipuladores temporários que não foram excluídos em nenhum lugar. Mas mais sobre isso mais tarde.
Isso pode ser usado? Ainda não totalmente implementado.
Removendo um manipulador dentro de um manipulador
Portanto, encontramos imediatamente uma falha durante a execução do código, em que o manipulador se desinscreve do evento (acho que não é o
caso de uso mais raro quando o manipulador se auto-corta sob quaisquer condições):
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 ); . . . }
O problema surge por uma razão muito simples:
- o evento é acionado e começa a iterar (usando iteradores) manipuladores, chamando-os;
- o próximo manipulador em si faz com que seja excluído;
- o evento exclui o manipulador fornecido, tornando o iterador correspondente inválido;
- após a conclusão desse manipulador, o evento volta a enumerar os outros; no entanto, o iterador atual (correspondente ao manipulador remoto) já é inválido;
- o evento está tentando acessar o iterador inválido, causando uma queda.
Portanto, é necessário verificar os casos em que a lista de manipuladores possa ser alterada, o que levaria à invalidação de iteradores; e, em seguida, implemente a proteção de leitura para esses iteradores.
A vantagem do
std :: list 'nesta aplicação é o fato de que, ao excluí-lo, apenas um iterador é inválido - no elemento excluído (afetando, por exemplo, o seguinte); e adicionar um elemento não leva à invalidação de nenhum iterador. Portanto, precisamos controlar o único caso: excluir um elemento cujo iterador é atual na enumeração atual de elementos. Nesse caso, você pode, por exemplo, não excluir um elemento, mas simplesmente marcar que o elemento atual deve ser excluído e deixá-lo dentro da enumeração de elementos.
Pode-se rolar imediatamente a implementação disso, mas proponho resolver esse problema juntamente com o seguinte.
Segurança da linha
Potencialmente, chamadas para três funções possíveis - adicionar, excluir e classificar (quando um evento é disparado) manipuladores - são possíveis a partir de diferentes threads em momentos aleatórios. Isso cria um campo inteiro de possibilidades para sua "interseção" no tempo, "sobreposição" da execução entre si e a queda do programa como resultado. Vamos tentar evitar isso;
Os mutexes são o nosso tudo .
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:
Não esqueça de deixar a janela aberta quase toda vez que ligar para cada manipulador. Isso é necessário para que dentro do manipulador seja possível acessar o evento e alterá-lo (por exemplo, adicionar / remover manipuladores) sem causar
conflito . Você não pode ter medo da validade dos dados, porque, como descobrimos, a única coisa que leva a isso é a exclusão do elemento atual, e essa situação foi processada.
UPD1. Obrigado,
Cheater ,
sugeriu que
std :: shared_mutex apareça apenas em
C ++ 17 (e
std :: shared_lock apenas em
C ++ 14 ). Aqueles para quem isso é crítico provavelmente terão a ver com
std :: mutex .
UPD2. Além disso, sobre segurança de threads (sem preservar a sequência da narração).
Problema de visibilidade do evento
Ao usar um evento como membro de uma classe, parece lógico torná-lo
público para que objetos de terceiros possam adicionar / remover seus manipuladores. No entanto, isso resultará em
operator () , ou seja, uma chamada de evento também estará acessível de fora, o que, em alguns casos, pode ser inaceitável. Resolveremos esse problema isolando da classe de eventos (
TEvent <...> ) uma interface abstrata destinada apenas ao tratamento de manipuladores.
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 {
Agora podemos dividir em escopos diferentes a parte do evento responsável por trabalhar com manipuladores e a parte responsável por chamá-lo.
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; . . . };
Portanto, agora objetos de terceiros podem adicionar / remover seus manipuladores via
TestWindow :: onButtonClick , mas eles não poderão disparar esse evento eles mesmos. Uma chamada agora pode ser realizada apenas dentro da classe
TestWindow (e seus descendentes, se o escopo do evento, como exemplo, estiver
protegido ).
O código trivial está lentamente começando a se transformar em algo monstruoso, mas esse não é o fim.
Parâmetros do evento de correspondência e seus manipuladores
Na implementação atual, o evento e qualquer um de seus manipuladores devem ter uma lista estritamente correspondente de parâmetros. Isso leva a uma série de desvantagens.
O primeiro. Suponha que tenhamos um modelo de classe no qual haja um evento com um parâmetro de modelo.
template<class TSource> class MyClass { . . . public: TEvent<const TSource&> onValueChanged; . . . };
Devido ao fato de que o tipo que será usado aqui não é conhecido antecipadamente, faz sentido transmiti-lo por link constante e não por valor. No entanto, agora para qualquer implementação, mesmo com tipos fundamentais, os manipuladores correspondentes devem estar presentes.
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 );
Gostaria de poder conectar manipuladores do formato
MyHandlerClass :: handleValueChanged2 a um evento semelhante, mas até agora não existe essa possibilidade.
O segundo Vamos tentar implementar um manipulador de functor semelhante a um método de manipulador existente (membro da função de uma classe).
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 )
Agora tente estragar tudo em algum evento.
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 ); . . . }
O resultado será um erro de compilação. Para a função
createFunctorEventHandler, o compilador não pode inferir os tipos de
TParams ... do único argumento para essa função - o próprio functor. O functor realmente não contém nenhuma informação sobre que tipo de manipulador criar com base nele. A única coisa que pode ser feita nessa situação é escrever algo como:
testWindow.onButtonClick += createFunctorEventHandler<ClickEventHandler, const std::string&, unsigned int>( clickEventHandler );
Mas você não quer fazer isso.
Conectando um evento a vários tipos de manipuladores
Portanto, há uma lista de desejos, depende da implementação. Vamos considerar a situação usando o exemplo de um manipulador de functor; um método manipulador (membro da função de uma classe) será obtido de maneira semelhante.
Como apenas com base em um functor é impossível dizer qual será a lista de parâmetros do manipulador correspondente, não faremos isso. Essa pergunta se torna relevante não no momento em que o manipulador foi criado, mas no momento de tentar anexá-lo a um evento específico. E sim, esses são dois pontos diferentes. Essa ideia pode ser implementada da seguinte maneira:
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; };
Em resumo, a separação dos momentos da criação do manipulador e sua ligação ao evento aqui é mais pronunciada do que antes. Isso contorna os problemas descritos no parágrafo anterior. O teste de compatibilidade de tipo ocorrerá ao tentar anexar um
FunctorHolder específico a um
FunctorEventHandler específico, ou melhor, crie uma instância da
classe FunctorEventHandler <...> com um tipo de functor muito específico; e nesta classe haverá uma linha de código
m_functorHolder.m_functor (params ...); , que simplesmente não pode ser compilado para um conjunto de tipos incompatíveis com um functor (ou se não é um functor, ou seja, um objeto que não possui
operador () ).
Repito que o problema de excluir objetos temporários será discutido abaixo. Além disso, vale ressaltar que um monte de macros para cada caso foi feito, primeiro, para demonstrar as capacidades desse tipo de manipuladores e, segundo, no caso de uma possível modificação de um deles com um arquivo.
Verifique o resultado.
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 );
Ocorre um erro de compilação ao tentar converter um dos parâmetros de
const lvalue para
lvalue . A conversão de
rvalue para
unconst lvalue não causa um erro, embora seja importante notar que ele cria uma ameaça potencial de auto-foto na perna: o manipulador poderá alterar a variável copiada na pilha, que será excluída com alegria quando esse manipulador sair.
Em geral, a mensagem de erro deve ser algo como isto:
Error C2664 'void Functor5::operator ()(std::string &,unsigned int &)': cannot convert argument 1 from 'const std::string' to 'std::string &'
Para maior clareza, ao usar eventos e manipuladores em código de terceiros, você pode adicionar sua própria mensagem de erro. Isso exigirá a criação de uma pequena estrutura de suporte (admito, espiei uma abordagem semelhante em algum lugar):
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... ); } . . . };
Este trabalho é baseado no mecanismo
SFINAE . Em resumo, é feita uma tentativa de compilar a primeira função, no entanto, se isso não funcionar devido à incompatibilidade dos argumentos (ou à ausência de
operador () do que foi passado como um functor), o compilador não gera um erro, mas simplesmente tenta compilar a segunda função; fazemos tudo para que sua compilação sempre seja bem-sucedida e, depois do fato de que função foi compilada, concluímos (escrevendo o resultado em
valor ) sobre a compatibilidade dos argumentos para os tipos fornecidos.
Agora, a mensagem de erro será mais ou menos assim:
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 &'
Além de uma mensagem de erro adicional e mais informativa, essa abordagem resolve o problema de converter argumentos de
rvalue para
unconst lvalue : agora, isso causa um erro de incompatibilidade de argumento, ou seja,
Tentar adicionar o manipulador
functor6 do exemplo acima resulta em um erro em tempo de compilação.
UPD Refinamento (sem preservar a sequência narrativa).
Comparação de Functor
Devido a alterações na classe manipuladora, a implementação da comparação de instâncias dessa classe mudará levemente. Mais uma vez, fornecerei uma implementação de apenas um manipulador de functor, porque o método do manipulador (membro da função) será semelhante.
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; . . . };
Com isso, as semelhanças na implementação da comparação terminam e a parte começa apenas para os manipuladores de functor.
Como observado acima, obtivemos vários tipos de manipuladores de functor: objetos diretamente functor, expressões lambda, instâncias da classe
std :: function , funções individuais. Desses, objetos functor, expressões lambda e instâncias da classe
std :: function não podem ser comparados usando o
operador == (eles precisam ser comparados no endereço), mas funções individuais podem, porque já armazenado em. Para não reescrever a função de comparação separadamente para cada caso, escrevemos em uma forma geral:
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 ); } }; }
Entende-se que is_equatable é um modelo auxiliar que determina se duas instâncias de um determinado tipo podem ser verificadas quanto à igualdade. Com sua ajuda, usando std :: enable_if , selecionamos uma das duas estruturas EqualityChecker parcialmente especializadas , que farão uma comparação: por valor ou por endereço. Is_equatable é implementado , pode ser o seguinte: 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; };
Essa implementação é baseada no mecanismo SFINAE , que já foi usado anteriormente . Somente aqui verificamos a presença do operador == para instâncias de uma determinada classe.Dessa maneira simples, a implementação da comparação de manipuladores-functores está pronta.Coleta de lixo
Seja indulgente, eu também queria inserir um título alto.Estamos chegando ao final e é hora de nos livrarmos do grande número de objetos criados que ninguém controla.Cada ação de evento com um manipulador cria dois objetos: Holder , que armazena a parte executável do manipulador, e EventHandlerconectando-o ao evento. Não esqueça que, no caso de uma tentativa de adicionar novamente o manipulador, nenhuma adição ocorrerá - dois objetos são "suspensos no ar" (a menos que, é claro, você não verifique esse caso separadamente todas as vezes). Outra situação: remover o manipulador; dois novos objetos também são criados para procurar o mesmo (igual) na lista de manipuladores de eventos; o manipulador encontrado da lista, é claro, é excluído (se houver), e este temporário, criado para a pesquisa e composto por dois objetos, está novamente "no ar". Em geral, não é legal.Vire para ponteiros inteligentes . Precisamos determinar qual será a semântica de propriedade de cada um dos dois objetos manipuladores: propriedade exclusiva ( std :: unique_ptr ) ou compartilhada ( std :: shared_ptr ).Titular, além de usar o próprio evento ao adicioná-lo / removê-lo, ele deve ser armazenado no EventHandler , portanto, usamos para propriedade compartilhada e, para o EventHandler, é único, porque após a criação, ele será armazenado apenas na lista de manipuladores de eventos.Percebemos esta ideia: 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 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 )
Primeiras coisas primeiro.Para começar, o evento e sua interface para trabalhar com manipuladores. No último, você não pode mais converter tipos diretamente usando static_cast , porque o tipo que está sendo convertido está “dentro” de std :: shared_ptr . Agora, para tal conversão, usaremos a estrutura auxiliar HandlerCast , que, por sua especialização particular, fornecerá acesso ao objeto dentro de std :: shared_ptr e, já trabalhando com ele (em sua implementação não especializada), aplicará o bom e velho static_cast .O evento em si; Existem várias mudanças importantes aqui também. Primeiro, paramos de excluir manualmente instâncias de manipulador no destruidor e ao excluir; agora basta remover o ponteiro inteligente com esse manipulador da lista. Além disso, ao adicionar um manipulador, é importante não esquecer std :: move , porque std :: unique_ptr não suporta cópia (o que é bastante lógico para essa semântica).Vamos passar para os manipuladores. Segundo a tradição antiga, apenas um deles é dado, o segundo é semelhante. E aqui, à primeira vista, tudo se resume a alterar os tipos de objetos armazenados / criados de links / ponteiros para ponteiros inteligentes.Mas há um ponto sutil. A função createMethodEventHandler retornará std :: shared_ptr para uma instânciaMethodHolder . Um pouco mais tarde, será feita uma tentativa de convertê-lo em um tipo de manipulador ( MethodEventHandler ), onde será necessário criar uma nova instância do MethodEventHandler , passando-a para o construtor std :: shared_ptr . Este é exatamente o objetivo da instância MethodHolder a ser excluída posteriormente quando a instância MethodEventHandler foi excluída . Mas o problema é que o MethodHolder não tem acesso ao std :: shared_ptr já criado que o armazena.Para resolver o problema, você deve armazenar um ponteiro inteligente para si mesmo no MethodHolder . No entanto, para que isso não afete sua remoção, usamosstd :: 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 ); }
Para maior clareza, darei uma ordem aproximada de eventos ao remover um manipulador de um evento (minhas desculpas por um trocadilho aleatório):- o evento remove o item da lista ( m_handlers.erase (it); ), que faz com que seu destruidor seja chamado;
- o destruidor std :: unique_ptr é chamado , o que leva a uma chamada ao destruidor do objeto gerenciado;
- o destruidor MethodEventHandler é chamado , que exclui todos os campos do objeto, incluindo o campo m_methodHolder , que é std :: shared_ptr ;
- std::shared_ptr ; , (.. ) ( MethodHolder ); , std::weak_ptr ;
- MethodHolder , , , m_me , std::weak_ptr ;
- std::weak_ptr ; ; porque std::weak_ptr , ;
- lucro.
É importante lembrar que o destruidor da classe AbstractEventHandler deve ser virtual; caso contrário, após a cláusula 2 na cláusula 3 , o destruidor AbstractEventHandler será chamado e outras ações não serão executadas.Conexão de evento e manipulador
Em alguns casos, quando a adição / remoção de um manipulador de um evento ocorre com bastante frequência (de acordo com alguma lógica), você não deseja se incomodar, obtendo uma instância do evento e uma instância do manipulador a cada vez, para implementar novamente uma assinatura / cancelamento da inscrição deste evento. Mas quero conectá-los uma vez e, se necessário, trabalhar com essa conexão, adicionando / removendo com ela um manipulador predefinido de um evento predefinido. Você pode implementar isso da seguinte maneira: 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:
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; };
Como você pode ver, agora foi adicionado outro local possível para armazenar a instância do manipulador; portanto, usaremos std :: shared_ptr em vez de std :: unique_ptr para isso .No entanto, esta classe, quanto a mim, é um pouco inconveniente de usar. Gostaria de armazenar e criar instâncias de conexão sem uma lista de parâmetros que instanciam o modelo de classe.Implementamos isso usando uma classe ancestral abstrata e um wrapper: 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 é a mesma estrutura de suporte usada aqui . A propósito, é importante não esquecer de tornar virtual o destruidor AbstractEventJoin para que, quando você excluir sua instância no destruidor EventJoinWrapper , o destruidor HandlerEventJoin seja chamado , caso contrário, o campo THandlerPtr e, portanto, o próprio manipulador não serão destruídos .Essa implementação parece ser viável, mas apenas à primeira vista. Copiar ou mover uma instância do EventJoinWrapper excluirá m_eventJoin em seu destruidor novamente . Portanto, usamos std :: shared_ptr para armazenar a instânciaAbstractEventJoin , bem como implementar semântica ligeiramente otimizada de movimento (e cópia), porque essa será uma operação potencialmente frequente. class EventJoinWrapper { public: EventJoinWrapper( EventJoinWrapper&& other ) : m_eventJoin( std::move( other.m_eventJoin ) ) { } EventJoinWrapper( EventJoinWrapper& other ) : m_eventJoin( other.m_eventJoin ) { } ~EventJoinWrapper() { } 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; };
Agora, ao conectar um manipulador a um evento, você pode retornar imediatamente uma instância de uma nova conexão: template<class ...TParams> class IEvent { . . . public: template<class TSome> EventJoin operator+=( TSome&& some ) { EventJoin result( *this, std::forward<TSome>( some ) ); result.join(); return result; } . . . };
Depois triangular razrulivaniya dependendo include'am (IEvent <= EventJointWrapper.hpp; EventJointWrapper < = HandlerEventJoin.hpp; HandlerEventJoin <= IEvent.hpp) compartilhar alguns arquivos para .h e .hpp Isso pode até funcionar.A criação de instâncias de conexão segue as mesmas regras que funcionam quando o manipulador de eventos assina: 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 ) );
Além disso, você pode "ativar" / "desativar" o processamento de eventos (para o qual, em princípio, as conexões foram criadas): 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.
Sumário
Primeiro, vale a pena notar que a tarefa de escrever um artigo o mais curto possível e sucintamente falhou completamente.Espero que a implementação resultante do processamento de eventos seja bastante funcional e seja útil para pelo menos alguém.Um exemplo muito complicado que demonstra os principais recursos #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; }
Conclusão:
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:
Vale a pena notar uma série de pontos importantes:- não foi especificado separadamente, portanto, mencionarei que o próprio evento nesta implementação é um functor, o que significa que ele pode atuar como manipulador de outro evento;
- agora você não pode usar métodos constantes (funções de membro de classe) como manipuladores; Eu acho que se tal oportunidade for necessária, não é difícil escrever um novo tipo de manipulador para isso com base nos já existentes.
Além disso, na versão final, existem alguns pontos omitidos no artigo para maior visibilidade e legibilidade do código:- o tipo do valor de retorno do método (função membro da classe) para o manipulador correspondente pode ser qualquer um, não necessariamente nulo (para manipuladores-functores, isso também foi feito);
- toda a implementação é agrupada em namespaces para facilitar o uso em projetos (se isso parecer supérfluo para alguém, você sempre pode removê-los);
- o especificador noexcept foi adicionado em alguns lugares .
Para todos aqueles que leram aqui pelo menos na diagonal, um arco baixo. Eu incluo todo o código; Também pode ser obtido aqui (com todas as melhorias mais recentes).Código inteiro./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(); }; }
./events/joins/abstracteventjoin.cpp #include "abstracteventjoin.h" namespace events { namespace joins { AbstractEventJoin::AbstractEventJoin() { } AbstractEventJoin::~AbstractEventJoin() { } }
./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. Aqui e no início do artigo, o código escrito em VC ++ 14 é fornecido . Para compatibilidade com outros compiladores, é melhor pegar o código no link. Agradecimentos especiais ao Cheater por fornecer compatibilidade com o GCC .UPD2. Agradecemos à lexxmark por observar uma falha na segurança da thread em termos de várias chamadas de evento simultâneas.Pequenas melhorias namespace { template<class ...TParams> struct TypeHelper { using TEventHandlerPtr = handlers::TEventHandlerPtr<TParams...>; using TEventHandlerIt = typename std::list<TEventHandlerPtr>::const_iterator; }; }
(, , )
HandlerRunner , . , :
currentIt ( )
wasRemoving (, ).
HandlerRunner' operator() ; (, ) ,
EventCore . T.O. , , , , , .
UPD3. Graças a isnullxbh , outro erro foi encontrado. É associado ao armazenamento inadequado e ao acesso subsequente aos objetos transmitidos pelo rvalue (principalmente expressões lambda).Correção,
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 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 ); }
Feito.
FunctorHolder . . - .
Comparação PS com o mecanismo de sinal / slot Qt
Acho que não me enganarei se disser que o Qt é uma estrutura muito comum para o desenvolvimento em C ++ . Entre outras coisas, ele também possui seu próprio mecanismo de processamento de eventos , no qual existem sinais como análogos de eventos e slots como análogos de manipuladores. É implementado usando o Meta-Object Compiler , que faz parte do Meta-Object System mais global , que, por sua vez, é implementado usando o complemento usado no Qt sobre C ++ .Recursos de ambas as implementações:
- a capacidade de conectar sinais (eventos) a métodos (funções-membro), functores e funções;
- () (), ( lvalue , rvalue );
- ( );
- () ( ).
Qt :
- ;
Qt ; «», , « » ; , ; , ;
- ;
Qt ( ) ( ); , Qt::UniqueConnection ; , , , Qt ;
- , ;
Qt::QueuedConnection Qt::BlockingQueuedConnection ; () ; (); , , ; isto é , ; , .
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 ;
- uso de código gerado adicionalmente pelo moc ;
isso já é bastante subjetivo, mas a decisão é: onde, para cada classe que usa sinais e slots (nem sempre os slots), existem vários arquivos gerados (por arquivo para cada configuração) que causam algum inconveniente; mas, para ser sincero, essa é a falha mais pequena.
É importante notar que essa comparação com o Qt é, no entanto, muito subjetiva e não visa exaltar ou condenar essa estrutura. Deve-se lembrar que, além do mecanismo de sinal / slot, o Qt oferece ótima funcionalidade, tanto usando esse mecanismo quanto não dependendo dele. De qualquer forma, é sempre sua decisão decidir o que usar e o que não usar.