Je pense que le traitement d'événements comme moyen d'interaction entre les objets dans la POO est connu de presque tous ceux qui ont déjà touché la POO au moins une fois. Au moins, cette approche est très pratique dans un éventail de tâches très large, à mon avis. Dans de nombreux langages de programmation, le moteur de gestion des événements est intégré; cependant, en C ++, il n'y a pas un tel mécanisme. Voyons ce que vous pouvez y faire.
Brève introduction
Un événement est quelque chose qui peut arriver à un objet dans certaines conditions (par exemple, avec un bouton lorsque vous cliquez dessus avec la souris). D'autres entités peuvent devoir en être conscientes; puis ils
s'inscrivent à l'événement . Dans ce cas, lorsqu'un événement se produit, le
gestionnaire d'un objet tiers abonné à l'événement est appelé; Ainsi, il a la possibilité d'exécuter du code, c'est-à-dire répondre à un événement. De même, un objet peut se
désinscrire d'un événement s'il ne souhaite plus y répondre. En conséquence, nous avons de nombreux objets qui peuvent être connectés les uns aux autres en utilisant les événements de l'un d'entre eux et la réaction à ces événements des autres.
Quelque chose comme ça, bien que tout le monde le sache.
Implémentation la plus simple
Il semblerait facile de mettre en œuvre un tel comportement. Et cela pourrait ressembler à ceci:
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 )
L'application de ce cas doit être de la forme:
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 ); . . . }
Naturellement, une méthode de gestionnaire (-fonction-membre d'une classe) ne sera pas le seul type de gestionnaires, mais plus à ce sujet plus tard.
Tout semble être pratique, compact et super. Mais s'il existe un certain nombre de lacunes.
Comparaison des gestionnaires
Pour implémenter la désinscription d'un événement, il est nécessaire d'ajouter la possibilité de comparaison au gestionnaire (par
== et
! == ). Les gestionnaires qui appellent la même méthode (une fonction membre d'une classe) du même objet (c'est-à-dire la même instance de la même classe) seront considérés comme égaux.
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; . . . };
Ensuite, nous pourrons supprimer les gestionnaires de l'abonnement à l'événement. Dans ce cas, il est nécessaire d'interdire l'ajout des mêmes (égaux) gestionnaires.
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; . . . };
Ici, les fonctions d'ajout / suppression du gestionnaire renvoient
true en cas de succès et
false si l'action correspondante (ajouter ou supprimer) n'a pas été effectuée.
Oui, le cas d'utilisation avec comparaison implique la création de gestionnaires temporaires, ajoutés nulle part qui ne sont supprimés nulle part. Mais plus à ce sujet plus tard.
Peut-il être utilisé? Pas encore entièrement mis en œuvre.
Suppression d'un gestionnaire dans un gestionnaire
Donc, nous rencontrons immédiatement un plantage lors de l'exécution du code, où le gestionnaire se désabonne de l'événement (je pense que ce n'est pas le
cas d'utilisation le plus rare lorsque le gestionnaire se coupe automatiquement dans toutes les conditions):
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 ); . . . }
Le problème se pose pour une raison très simple:
- l'événement est déclenché et commence à itérer (à l'aide d'itérateurs) les gestionnaires, les appelant;
- le gestionnaire suivant en lui-même entraîne sa suppression;
- l'événement supprime le gestionnaire donné, rendant l'itérateur correspondant invalide;
- après l'achèvement de ce gestionnaire, l'événement revient à énumérer les autres, cependant, l'itérateur actuel (correspondant au gestionnaire distant) est déjà invalide;
- l'événement tente d'accéder à l'itérateur non valide, provoquant une chute.
Par conséquent, il est nécessaire de vérifier les cas où la liste des gestionnaires peut être modifiée, ce qui entraînerait l'invalidation des itérateurs; puis implémentez la protection en lecture pour ces itérateurs.
L'avantage de
std :: list 'dans cette application est le fait que lors de sa suppression, un seul itérateur n'est pas valide - sur l'élément supprimé (affectant, par exemple, les éléments suivants); et l'ajout d'un élément n'entraîne pas du tout l'invalidation d'itérateurs. Ainsi, nous devons contrôler le seul cas: supprimer un élément dont l'itérateur est courant dans l'énumération courante des éléments. Dans ce cas, vous pouvez, par exemple, ne pas supprimer un élément, mais simplement marquer que l'élément actuel doit être supprimé, et le laisser se faire à l'intérieur de l'énumération des éléments.
Il serait possible de déployer immédiatement la mise en œuvre de cela, mais je propose de résoudre ce problème avec les éléments suivants.
Sécurité des fils
Potentiellement, les appels à trois fonctions possibles - ajouter, supprimer et trier (lorsqu'un événement est déclenché) des gestionnaires - sont possibles à partir de différents threads à des moments aléatoires. Cela crée tout un champ de possibilités pour leur «intersection» dans le temps, le «chevauchement» de leur exécution les uns sur les autres et la chute du programme en conséquence. Essayons d'éviter cela;
les mutex sont notre tout .
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'oubliez pas de laisser la fenêtre ouverte presque chaque fois que vous appelez chaque gestionnaire. Cela est nécessaire pour qu'à l'intérieur du gestionnaire, il soit possible d'accéder à l'événement et de le modifier (par exemple, ajouter / supprimer des gestionnaires) sans provoquer d'
interblocage . Vous ne pouvez pas avoir peur de la validité des données, car, comme nous l'avons découvert, la seule chose qui y mène est la suppression de l'élément actuel, et cette situation a été traitée.
UPD1. Merci
Cheater , a
suggéré que
std :: shared_mutex n'apparaisse qu'en
C ++ 17 (et
std :: shared_lock uniquement en
C ++ 14 ). Ceux pour qui cela est critique auront probablement à faire avec
std :: mutex .
UPD2. De plus, sur la sécurité des threads (sans conserver la séquence de la narration).
Problème de visibilité des événements
Lorsque vous utilisez un événement en tant que membre d'une classe, il semble logique de le rendre
public , afin que les objets tiers puissent ajouter / supprimer leurs gestionnaires. Cependant, cela entraînera l'
opérateur () , c'est-à-dire un appel d'événement sera également accessible de l'extérieur, ce qui dans certains cas peut être inacceptable. Nous allons résoudre ce problème en isolant de la classe d'événements (
TEvent <...> ) une interface abstraite destinée uniquement à la gestion des gestionnaires.
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 {
Nous pouvons maintenant diviser en différentes étendues la partie de l'événement chargée de travailler avec les gestionnaires et la partie chargée de l'appeler.
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; . . . };
Ainsi, les objets tiers peuvent désormais ajouter / supprimer leurs gestionnaires via
TestWindow :: onButtonClick , mais ils ne pourront pas déclencher cet événement eux-mêmes. Un appel ne peut désormais être effectué qu'à l'intérieur de la classe
TestWindow (et de ses descendants, si la portée de l'événement, par exemple, est
protégée ).
Le code trivial commence lentement à devenir quelque chose de monstrueux, mais ce n'est pas la fin.
Faire correspondre les paramètres d'événement et ses gestionnaires
Dans l'implémentation actuelle, l'événement et l'un de ses gestionnaires doivent avoir une liste de paramètres strictement correspondante. Cela entraîne un certain nombre d'inconvénients.
Le premier. Supposons que nous ayons un modèle de classe dans lequel il y a un événement avec un paramètre de modèle.
template<class TSource> class MyClass { . . . public: TEvent<const TSource&> onValueChanged; . . . };
Étant donné que le type qui sera utilisé ici n'est pas connu à l'avance, il est logique de le passer par un lien constant et non par valeur. Cependant, maintenant pour toute implémentation, même avec des types fondamentaux, les gestionnaires correspondants doivent être présents.
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 );
Je voudrais pouvoir connecter des gestionnaires de la forme
MyHandlerClass :: handleValueChanged2 à un événement similaire, mais jusqu'à présent il n'y a pas une telle possibilité.
Le deuxième. Essayons d'implémenter un gestionnaire de foncteurs similaire à une méthode de gestionnaire existante (-function-member d'une 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 )
Essayez maintenant de le visser à un événement.
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 ); . . . }
Le résultat sera une erreur de compilation. Pour la fonction
createFunctorEventHandler, le compilateur ne peut pas déduire les types de
TParams ... du seul argument à cette fonction - le foncteur lui-même. Le foncteur ne contient vraiment aucune information sur le type de gestionnaire à créer en fonction de celui-ci. La seule chose qui peut être faite dans cette situation est d'écrire quelque chose comme:
testWindow.onButtonClick += createFunctorEventHandler<ClickEventHandler, const std::string&, unsigned int>( clickEventHandler );
Mais vous ne voulez pas du tout faire cela.
Connexion d'un événement à différents types de gestionnaires
Donc, il y a une liste de souhaits, c'est à la mise en œuvre. Nous considérerons la situation en utilisant l'exemple d'un gestionnaire de foncteurs; une méthode de gestionnaire (-fonction-membre d'une classe) sera obtenue de manière similaire.
Étant donné que sur la base d'un foncteur seul, il est impossible de dire quelle sera la liste des paramètres du gestionnaire correspondant, nous ne le ferons pas. Cette question devient pertinente non pas au moment où le gestionnaire a été créé, mais au moment d'essayer de l'attacher à un événement spécifique. Et oui, ce sont deux points différents. Cette idée peut être mise en œuvre comme suit:
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; };
Bref, la séparation des moments de la création du gestionnaire et de son attachement à l'événement ici est plus prononcée qu'auparavant. Cela contourne les problèmes décrits dans le paragraphe précédent. Des tests de compatibilité de type se produiront lorsque vous
tenterez d' attacher un
FunctorHolder spécifique à un
FunctorEventHandler spécifique, ou plutôt de créer une instance de la
classe FunctorEventHandler <...> avec un type de
functor très spécifique; et dans cette classe il y aura une ligne de code
m_functorHolder.m_functor (params ...); , qui ne peut tout simplement pas être compilé pour un ensemble de types incompatibles avec un foncteur (ou s'il n'est pas du tout un foncteur, c'est-à-dire un objet qui n'a pas d'
opérateur () ).
Je répète que le problème de la suppression d'objets temporaires sera discuté ci-dessous. De plus, il convient de noter qu'un tas de macros pour chaque cas a été réalisé, d'une part, afin de démontrer les capacités de ce type de gestionnaires, et d'autre part, en cas d'une éventuelle modification de l'un d'entre eux avec un fichier.
Vérifiez le résultat.
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 );
Une erreur de compilation se produit lors de la tentative de conversion de l'un des paramètres de
const lvalue en
lvalue . La conversion de
rvalue en
unconst lvalue ne provoque pas d'erreur, bien qu'il soit intéressant de noter que cela crée une menace potentielle de self-shot dans la jambe: le gestionnaire pourra changer la variable copiée sur la pile, qui sera joyeusement supprimée à sa sortie.
En général, le message d'erreur devrait ressembler à ceci:
Error C2664 'void Functor5::operator ()(std::string &,unsigned int &)': cannot convert argument 1 from 'const std::string' to 'std::string &'
Pour plus de clarté, lorsque vous utilisez des événements et des gestionnaires dans du code tiers, vous pouvez ajouter votre propre message d'erreur. Cela nécessitera l'écriture d'une petite structure de support (j'avoue, j'ai espionné une approche similaire quelque part):
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... ); } . . . };
Ce travail est basé sur le mécanisme
SFINAE . En bref, une tentative de compilation de la première fonction
existe , cependant, si cela ne fonctionne pas en raison d'une incompatibilité des arguments (ou de l'absence d'
opérateur () de ce qui a été passé en tant que foncteur), le compilateur ne renvoie pas d'erreur, mais essaie simplement de compiler la deuxième fonction; nous faisons tout pour que sa compilation réussisse toujours, puis, sur le fait de la fonction qui a été compilée, nous concluons (en écrivant le résultat à
value ) sur la compatibilité des arguments pour les types donnés.
Maintenant, le message d'erreur ressemblera à ceci:
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 &'
En plus d'un message d'erreur supplémentaire et plus informatif, cette approche résout le problème de la conversion des arguments de
rvalue en
unconst lvalue : maintenant, elle provoque une erreur d'incompatibilité d'argument, c'est-à-dire
Tenter d'ajouter le gestionnaire
functor6 de l'exemple ci-dessus entraîne une erreur de compilation.
UPD Raffinement (sans conserver la séquence de la narration).
Comparaison des foncteurs
En raison des changements dans la classe du gestionnaire, l'implémentation de la comparaison des instances de cette classe changera légèrement. Encore une fois, je fournirai une implémentation d'un gestionnaire de foncteurs uniquement, car la méthode du gestionnaire (-function-member de la classe) sera similaire.
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; . . . };
Sur ce point, les similitudes dans l'implémentation de la comparaison se terminent et la partie ne commence que pour les gestionnaires de foncteurs.
Comme indiqué ci-dessus, nous avons obtenu plusieurs types de gestionnaires de foncteurs: directement les objets foncteurs, les expressions lambda, les instances de la classe
std :: function , les fonctions individuelles. Parmi ceux-ci, les objets foncteurs, les expressions lambda et les instances de la classe
std :: function ne peuvent pas être comparés à l'aide de l'
opérateur == (ils doivent être comparés à l'adresse), mais les fonctions individuelles le peuvent, car déjà stocké à. Afin de ne pas réécrire la fonction de comparaison séparément pour chaque cas, nous l'écrivons sous une forme générale:
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 ); } }; }
Il est entendu que is_equatable est un modèle auxiliaire qui détermine si l'égalité de deux instances d'un type donné peut être vérifiée. Avec son aide, en utilisant std :: enable_if , nous sélectionnons l'une des deux structures EqualityChecker partiellement spécialisées , qui effectueront une comparaison: par valeur ou par adresse. Is_equatable est implémenté , il peut être le suivant: 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; };
Cette implémentation est basée sur le mécanisme SFINAE , déjà utilisé précédemment . Seulement ici, nous vérifions la présence de l' opérateur == pour les instances d'une classe donnée.De cette manière simple, l'implémentation de la comparaison des gestionnaires-foncteurs est prête.Collecte des ordures
Soyez indulgents, je voulais aussi insérer un gros titre.Nous approchons de la finale, et il est temps de se débarrasser du grand nombre d'objets créés que personne ne contrôle.Chaque action d'événement avec un gestionnaire crée deux objets: Holder , qui stocke la partie exécutable du gestionnaire, et EventHandlerle connecter à l'événement. N'oubliez pas qu'en cas de tentative de rajout du gestionnaire, aucun ajout ne se produira - deux objets sont «suspendus dans les airs» (à moins, bien sûr, que vous ne vérifiiez pas ce cas séparément à chaque fois). Autre situation: suppression du gestionnaire; deux nouveaux objets sont également créés pour rechercher le même (égal) dans la liste des gestionnaires d'événements; le gestionnaire trouvé dans la liste, bien sûr, est supprimé (le cas échéant), et celui temporaire, créé pour la recherche et composé de deux objets, est à nouveau "dans les airs". En général, pas cool.Tournez-vous vers des pointeurs intelligents . Vous devez déterminer quelle sera la sémantique de propriété de chacun des deux objets gestionnaire: propriété unique ( std :: unique_ptr ) ou partagée ( std :: shared_ptr ).Titulaire, en plus d'être utilisé par l'événement lui-même lors de son ajout / suppression, il doit être stocké dans le gestionnaire d'événements , par conséquent, nous l'utilisons pour la propriété partagée, et pour le gestionnaire d'événements, il est unique, car après sa création, il sera stocké uniquement dans la liste des gestionnaires d'événements.Nous réalisons cette idée: 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 )
Tout d'abord.Pour commencer, l'événement et son interface pour travailler avec les gestionnaires. Dans ce dernier, vous ne pouvez plus convertir les types directement en utilisant static_cast , car le type en cours de conversion se trouve «à l'intérieur» std :: shared_ptr . Maintenant, pour une telle conversion, nous utiliserons la structure auxiliaire HandlerCast , qui, par sa spécialisation privée, donnera accès à l'objet à l'intérieur de std :: shared_ptr , et déjà en travaillant avec lui (dans son implémentation non spécialisée), il appliquera le bon vieux static_cast .L'événement lui-même; Il y a également plusieurs changements importants. Premièrement, nous arrêterons de supprimer manuellement les instances de gestionnaire dans le destructeur et lors de la suppression; il suffit maintenant de supprimer le pointeur intelligent avec ce gestionnaire de la liste. De plus, lors de l'ajout d'un gestionnaire, il est important de ne pas oublier std :: move , car std :: unique_ptr ne prend pas en charge la copie (ce qui est assez logique pour une telle sémantique).Passons aux gestionnaires. Selon l'ancienne tradition, un seul d'entre eux est donné, le second est similaire. Et ici, à première vue, tout se résume à changer les types d'objets stockés / créés de liens / pointeurs en pointeurs intelligents.Mais il y a un point subtil. La fonction createMethodEventHandler retournera std :: shared_ptr à une instanceMethodHolder . Un peu plus tard, une tentative sera faite pour le convertir en un type de gestionnaire ( MethodEventHandler ), où il devra créer une nouvelle instance de MethodEventHandler , en la passant au constructeur std :: shared_ptr sur lui-même. C'est exactement ce qui était prévu pour que l'instance de MethodHolder soit supprimée ultérieurement lorsque l'instance de MethodEventHandler a été supprimée . Mais le problème est que MethodHolder n'a pas accès au std :: shared_ptr déjà créé qui le stocke lui-même.Pour résoudre le problème, vous devez stocker un pointeur intelligent vers vous-même dans MethodHolder . Cependant, afin qu'il n'affecte pas sa suppression, nous utilisonsstd :: faible_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 ); }
Pour plus de clarté, je donnerai un ordre approximatif d'événements lors de la suppression d'un gestionnaire d'un événement (mes excuses pour un jeu de mots aléatoire):- l'événement supprime l'élément de la liste ( m_handlers.erase (it); ), ce qui provoque l'appel de son destructeur;
- le destructeur std :: unique_ptr est appelé , ce qui conduit à un appel au destructeur de l'objet géré;
- le destructeur MethodEventHandler est appelé , ce qui supprime tous les champs de l'objet, y compris le champ m_methodHolder , qui est std :: shared_ptr ;
- std::shared_ptr ; , (.. ) ( MethodHolder ); , std::weak_ptr ;
- MethodHolder , , , m_me , std::weak_ptr ;
- std::weak_ptr ; ; .. std::weak_ptr , ;
- .
Il est important de se rappeler que le destructeur de la classe AbstractEventHandler doit être virtuel; sinon, après la clause 2 de la clause 3 , le destructeur AbstractEventHandler sera appelé et aucune autre action ne sera effectuée.Connexion d'événement et de gestionnaire
Dans certains cas, lorsque l'ajout / la suppression d'un gestionnaire d'un événement se produit assez souvent (selon une logique), vous ne voulez pas vous embêter, obtenir une instance de l'événement et une instance du gestionnaire à chaque fois, pour implémenter à nouveau un abonnement / désabonnement à cet événement. Mais je veux les connecter une fois, puis, si nécessaire, travailler avec cette connexion, en ajoutant / supprimant avec elle un gestionnaire prédéfini d'un événement prédéfini. Vous pouvez l'implémenter comme suit: 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; };
Comme vous pouvez le voir, un autre endroit possible pour stocker l'instance du gestionnaire a été ajouté, nous allons donc utiliser std :: shared_ptr au lieu de std :: unique_ptr pour cela .Cependant, cette classe, quant à moi, est légèrement gênante à utiliser. Je voudrais stocker et créer des instances de connexion sans liste de paramètres qui instancient le modèle de classe.Nous implémentons cela en utilisant une classe ancêtre abstraite et un 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 est la même structure de support qui a été utilisée ici . À propos, il est important de ne pas oublier de rendre le destructeur AbstractEventJoin virtuel afin que lorsque vous supprimez son instance dans le destructeur EventJoinWrapper , le destructeur HandlerEventJoin soit appelé , sinon le champ THandlerPtr et, par conséquent, le gestionnaire lui - même ne seront pas détruits .Cette implémentation semble être réalisable, mais seulement à première vue. La copie ou le déplacement d'une instance d' EventJoinWrapper supprimera à nouveau m_eventJoin dans son destructeur. Par conséquent, nous utilisons std :: shared_ptr pour stocker l'instanceAbstractEventJoin , ainsi que la mise en œuvre d'une sémantique légèrement optimisée du mouvement (et de la copie), car ce sera une opération potentiellement fréquente. 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; };
Maintenant, lorsque vous connectez un gestionnaire à un événement, vous pouvez immédiatement renvoyer une instance d'une nouvelle connexion: template<class ...TParams> class IEvent { . . . public: template<class TSome> EventJoin operator+=( TSome&& some ) { EventJoin result( *this, std::forward<TSome>( some ) ); result.join(); return result; } . . . };
Et après avoir résolu la dépendance triangulaire par include (IEvent <= EventJointWrapper.hpp; EventJointWrapper <= HandlerEventJoin.hpp; HandlerEventJoin <= IEvent.hpp) en divisant certains fichiers en .h et .hpp, vous pouvez même travailler avec cela.La création d'instances de connexion suit les mêmes règles qui fonctionnent lorsque le gestionnaire d'événements s'abonne: 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 ) );
De plus, vous pouvez "activer" / "désactiver" le traitement des événements (pour lesquels, en principe, des connexions ont été créées): 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.
Résumé
Tout d'abord, il convient de noter que la tâche d'écrire un article aussi court que possible et succinctement complètement échoué.J'espère que l'implémentation résultante du traitement des événements est assez fonctionnelle et sera utile à au moins quelqu'un.Un exemple très lourd qui montre les principales fonctionnalités #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; }
Conclusion:
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:
Il convient de noter un certain nombre de points importants:- il n'a pas été spécifié séparément. Je mentionnerai donc que l'événement lui-même dans cette implémentation est un foncteur, ce qui signifie qu'il peut agir comme gestionnaire pour un autre événement;
- maintenant, vous ne pouvez pas utiliser de méthodes constantes (fonctions membres de classe) comme gestionnaires; Je pense que si une telle opportunité est nécessaire, il n'est pas difficile d'écrire un nouveau type de gestionnaire pour cela basé sur ceux existants.
De plus, dans la version finale, certains points sont omis dans l'article pour une meilleure visibilité et lisibilité du code:- le type de la valeur de retour de la méthode (fonction membre de la classe) pour le gestionnaire correspondant peut être quelconque, pas nécessairement nul (pour les gestionnaires-foncteurs, cela a également été fait)
- l'implémentation entière est enveloppée dans des espaces de noms pour une facilité d'utilisation dans les projets (si cela semble superflu pour quelqu'un, vous pouvez toujours les supprimer);
- le spécificateur noexcept a été ajouté à certains endroits .
À tous ceux qui ont lu ici au moins en diagonale, un arc bas. Je joins tout le code; il peut également être pris ici (avec toutes les dernières améliorations).Code entier./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. Ici et plus haut dans l'article, le code écrit sous VC ++ 14 est donné . Pour la compatibilité avec d'autres compilateurs, il est préférable de prendre le code du lien. Un merci spécial à Cheater pour avoir fourni la compatibilité avec le GCC .UPD2. Merci lexxmark d' avoir remarqué un trou de sécurité dans les threads en termes d'appels d'événements simultanés multiples.Améliorations mineures 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. Grâce à isnullxbh , une autre erreur a été trouvée. Il est associé à un stockage incorrect et à un accès ultérieur aux objets transmis par rvalue (principalement des expressions lambda).Correction,
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 ); }
C'est fait.
FunctorHolder . . - .
Comparaison PS avec le mécanisme de signal / slot Qt
Je pense que je ne me tromperai pas si je dis que Qt est un framework très courant pour le développement en C ++ . Entre autres choses, il possède également son propre mécanisme de traitement d'événements , dans lequel il existe des signaux en tant qu'analogues d'événements et des créneaux en tant qu'analogues de gestionnaires. Il est implémenté à l'aide du compilateur de méta-objets , qui fait partie du système de méta-objets plus global , qui, à son tour, est implémenté à l'aide du module complémentaire utilisé dans Qt sur C ++ .Caractéristiques des deux implémentations:
- la capacité de connecter des signaux (événements) à des méthodes (fonctions membres), des foncteurs et des fonctions;
- () (), ( lvalue , rvalue );
- ( );
- () ( ).
Qt :
- ;
Qt ; «», , « » ; , ; , ;
- ;
Qt ( ) ( ); , Qt::UniqueConnection ; , , , Qt ;
- , ;
Qt::QueuedConnection Qt::BlockingQueuedConnection ; () ; (); , , ; c'est-à-dire , ; , .
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 ;
- utilisation de code généré en plus par moc ;
c'est déjà complètement subjectif, mais la décision est, où pour chaque classe qui utilise des signaux et des slots (les slots ne le sont pas toujours) il y a plusieurs fichiers générés (par fichier pour chaque configuration) qui causent des inconvénients; mais pour être honnête, c'est le défaut le plus mineur.
Il est important de noter que cette comparaison avec Qt est néanmoins très subjective et ne vise pas à exalter ou condamner ce cadre. Il faut se rappeler qu'en plus du mécanisme signal / slot, Qt offre une grande fonctionnalité, à la fois en utilisant ce mécanisme et non en fonction de celui-ci. Dans tous les cas, c'est toujours à vous de décider quoi utiliser ou non.