boost.Signals, deuxième partie !

Pouet ! Bon, dans l’article précédent, j’expliquais comment boost a changé ma vie et tout et tout. Après avoir fait mon petit post, je suis retourné sur mon projet et ai commencé à supprimer le code que j’utilisais pour tout remplacer par boost. Tout ce passe bien jusqu’au moment où je tombe sur mon callback de gestion des messages clavier / souris … Et là, le bloquage : dans son utilisation “simple”, boost ne permet pas de gérer une fonctionnalité dont j’ai besoin dans ce cas particulier !

Explication : dans la plupart des cas, on balance un signal, et tous les slots sont appelés. C’est très bien. Mais dans certains cas, on voudrait récupérer la liste des slots et pour chacun, tester la valeur de retour avant d’envoyer le signal au slot suivant. Par exemple, dans mon cas, quand on traite les messages de la souris, à partir du moment où un slot me renvoie une valeur signifiant “hey c’est bon, c’est moi qui gère le message !” j’ai pas besoin de continuer d’envoyer le signal aux suivants, vu que le message a déjà été géré.

Bon, et bien boost a une fonctionnalité un peu plus évoluée qui s’appelle les “combiners” et qui permet d’associer au signal une fonction objet qui définit la manière dont on traite les slots. C’est pas forcément super clair, donc ci-dessous, un petit exemple :

/*
    On définit une structure template qui va ensuite être associée à notre
    signal lors de sa création. Cette structure doit contenir une définition
    de type et un operator ().
*/
template
struct MsgProcStop
{
    /*
        définit le type de retour du combiner. En général, c'est le même
        que le type de retour des slots, mais via les combiners, on peut
        retourner ce qu'on veut. Par exemple, les fonctions de gestion
        des messages retournent en général un entier, mais on peut
        très bien décider de retourner true si le signal a été géré par un
        slot ou false sinon (donc retourner un bool à la place d'un int)
    */
    typedef T result_type;

    /*
        L'operator () Celui-ci adment 2 iterateurs, tout simplement sur
        le premier et le dernier slot du signal. C'est appelé une seule fois,
        et du coup, on fait ce qu'on veut à l'intérieur.
    */
    template < typename InputIterator >
    T operator()(InputIterator first, InputIterator last) const
    {
        /* cette ligne vérifie simplement qu'il y a au moins un slot à traiter. */
        if (first == last)
            return(T());

        /*
            on initialise la boucle : cette ligne récupère la valeur de retour
            du slot, et déplace celui-ci d'un cran. Je ne sais pas si c'est
            l'opérateur ++ ou * qui exécute implicitement le slot, mais bon,
            l'important, c'est qu'il est appelé :)
        */
        T returnCode = *first++;

        /* Ensuite, on se déplace jusqu'à la fin de la liste de slots */
        while (first != last)
        {
            /*
                dans notre cas, on teste si la valeur de retour est différente
                de zéro. Si c'est le cas, ça veut dire que le message a été géré,
                et donc on s'arrête là, et on retourne le résultat (on pourrait
                retourner true ou false si on choisit bool comme type de retour
                du combiner ...)
            */
            if (returnCode != 0)
                return(returnCode);

            /* on passe au slot suivant */
            returnCode = *first++;
        }

        /* si on arrive là, on peut retourner la dernière valeure, ou un truc */
        /* par défaut ... on s'en fout. */
        return(returnCode);
    }

};


/* le signal est définit comme suit (le constructeur du signal peut être laissé vide */
/* dans ce cas, car par défaut il appelle le constructeur vide du combiner) */
boost::signal< int (), MsgProc< int > > signal(MsgProc< int >());

Bon, si vous avez lu les commentaires, vous devriez à peut près comprendre comment ça marche. Le combiner est en fait la fonction objet qui définit comment sont appelés les slots, et quoi faire avec leurs valeurs de retour. Donc ici, chaque slot va recevoir le signal, exécuter la fonction qui lui est associée, et dès qu’un slot retourne une valeur différente de zéro, hop, on arrête d’envoyer le signal ! C’est pil poil ce qu’on voulait faire.

Mais ! Problème : le test est codé en dur … ce qui est pète couilles, vu que si on veut tester une égalité, ou tester contre une autre valeur que 0, il faudrait dupliquer toute la classe … pas glop. Donc en cherchant dans mon pitit cerveau tout moisi (oui, ça fait longtemps que je ne l’utilise plus beaucoup, du coup …) j’ai trouvé la parade ultime ! boost::function !

Il y a deux choses à gérer pour rendre notre combiner vraiment portable : l’opérateur de test (le != dans notre exemple) et la valeur testée (0 dans notre exemple) Pour gérer la valeur, pas compliqué, on la stocke en variable membre dans le combiner, et on l’initialise via un constructeur. Et pour l’opérateur … et bien il suffit d’utiliser une fonction objet que l’on stocke aussi en variable membre ! Bon, démonstration passque c’est pas très clair tout ça :

template
struct MsgProcStop
{
    /*
        on créer un constructeur qui permet de spécifier la fonction de
        comparaison, et la valeur de référence. Voir plus bas pour la définition
        de ces fonctions, et pour l'utilisation des 2
    */
    MsgProcStop(const T & compareValue,
                      const boost::function< bool (T, T) > & compareFunction) :
        CompareValue(compareValue),
        CompareFunction(compareFunction)
    {
    }

    typedef T result_type;

    template < typename InputIterator >
    T operator()(InputIterator first, InputIterator last) const
    {
        if (first == last)
            return(T());

        T returnCode = *first++;
        while (first != last)
        {
            /*
                Et hop ! C'est ici que ça se passe ! Au lieu d'avoir le test et la
                valeur de référence en dur, on appelle maintenant la fonction
                objet de comparaison, et on lui donne la valeur de référence,
                et le code de retour du slot.
            */
            if (CompareFunction(returnCode, CompareValue) == true)
                return(returnCode);

            returnCode = *first++;
        }

        return(returnCode);
    }

    /*
        Ici, on définit des fonction objet qui reprennent simplement les
        différents opérateurs de test que l'on veut gérer.
    */
    struct Equal        { bool operator()(T & lVal, T & rVal) { return(lVal == rVal); } };
    struct Different    { bool operator()(T & lVal, T & rVal) { return(lVal != rVal); } };
    struct Less         { bool operator()(T & lVal, T & rVal) { return(lVal < rVal); } };
    struct Greater      { bool operator()(T & lVal, T & rVal) { return(lVal > rVal); } };
    struct LessEqual    { bool operator()(T & lVal, T & rVal) { return(lVal <= rVal); } };
    struct GreaterEqual { bool operator()(T & lVal, T & rVal) { return(lVal >= rVal); } };

    /* une fonction objet qui retourne un bool et prend en argument */
    /* 2 valeurs de type T */
    boost::function< bool (T, T) > CompareFunction;

    /* la valeur contre laquelle on effectue le test */
    T CompareValue;
};

/* maintenant, la définition des signaux est un chouilla plus complexe, mais */
/* complètement "libre : */
boost::signal< int (), MsgProcStop< int > > signal(MsgProcStop< int >(10,
                                   MsgProcStop< int >::OperatorEqual()));
boost::signal< int (), MsgProcStop< int > > signal(MsgProcStop< int >(0,
                                   MsgProcStop< int >::OperatorDifferent()));

Ouf, on arrive à la fin. Bon, les 2 signaux créés à la fin de l’exemple ci-dessus utilisent notre nouveau combiner : le premier utilise la valeur 10 et l’operateur ==, donc dès qu’un slot renvoie 10, le signal est stoppé. Le deuxième s’arrête dès qu’un slot renvoie une valeur différente de 0. Et on peut combiner tout ce qu’on veut lors de la création des signaux. Dingue non ? Bon, là, mon combiner s’appelle MsgProcStop … maintenant qu’il est parfaitement portable et configurable, je devrais plutôt l’appeler StopSignal ou un truc du genre, vu qu’il permet de stopper un signal sous certaines conditions.

Donc voilà tout, maintenant, via boost::signal, boost::bind, boost::function et cette petite classe combiner toute con, on a un système de gestion d’évènement configurable à volonté, facile d’utilisation et tout et tout. Et si la syntaxe est un peu pourrite lors de la création d’un signal utilisant un combiner, c’est pas trop grave vu que ça n’arrive pas souvent, et que de toute manière, la connexion d’un slot se fait de la même manière que sans combiner, donc c’est transparent pour l’utilisation !

One thought on “boost.Signals, deuxième partie !

Comments are closed.