Pile d'évènements génériques avec std::function

Posted on Wed 05 March 2014 in code

Ces derniers temps j'ai cherché un moyen générique de faire une pile d'évènements. Plus précisément, je souhaite garder dans une pile des fonctions à appeler selon certaines conditions. Un moyen le plus orienté-objet et facile à mettre en place est le design pattern Commande ou une simple classe abstraite avec une méthode virtuelle pure à exécuter. Seulement dans mon cas, ça nécessitait de générer beaucoup de classes pour juste une méthode, ce que je ne souhaitais pas.

J'ai donc rusé, comme j'ai un lourd passé de développeur C, j'ai pensé à une espèce de pointeur de fonction destinée à être appelée quand c'est nécessaire. Le problème c'est que cela requiert une signature identique pour toutes les fonctions, ce qui n'est pas mon cas. Mais grâce à std::function et à std::bind on peut très bien préparer des fonctions à signature différentes.

Exemple 1, une fonction simple à exécuter

Dans cet exemple, nous prenons une simple commande à exécuter dans l'ordre de la pile.

#include <functional>
#include <iostream>
#include <queue>
#include <string>

class Command {
private:
    std::string m_name;

public:
    Command(const std::string &name)
        : m_name(name)
    {
    }

    void send(const std::string &text, int count)
    {
        std::cout << "I'm " << m_name << std::endl;
        std::cout << "Sending '" << text << "' ";
        std::cout << count << " time(s)" << std::endl;
    }

    void move(int x, int y)
    {
        std::cout << "I'm " << m_name << std::endl;
        std::cout << "Moving to " << x << ", " << y << std::endl;
    }
};

using Call  = std::function<void ()>;
using Queue = std::queue<Call>;

int main() {
    Queue queue;
    Command c1("Creator");
    Command c2("Terminator");

    queue.push(std::bind(&Command::send, c1, "hello", 5));
    queue.push(std::bind(&Command::move, c2, -19, -84));

    while (queue.size() > 0) {
        queue.front()();
        queue.pop();
    }

    return 0;
}

Tout d'abord, la classe Command, elle n'est pas très complexe et possède deux fonctions membres. Nous allons décider d'empiler un appel à send pour la commande c1 puis ensuite à move pour la commande c2. Regardons maintenant le prototype de la fonction à empiler :

using Call = std::function<void ()>

Rien de bien complexe, cela signifie que lorsque nous déciderons de l'exécuter, il nous suffira de l'appeler sans paramètres. Voici maintenant l'empilement des fonctions:

queue.push(std::bind(&Command::send, c1, "hello", 5));
queue.push(std::bind(&Command::move, c2, -19, -84));

La fonction std::bind va prendre en paramètre un pointeur de fonction membre à appeler ainsi que l'instance de l'objet. On rajoute ensuite les paramètres qui sont déjà appliqués et seront "stockés" jusqu'à la destruction de l'objet retourné par std::bind. On précise dans la ligne 1 que c'est avec l'instance c1 que nous voulons exécuter la commande send, et avec c2 que nous voulons exécuter move. Pour terminer, il suffit d'appeler comme l'indique la signature de Call nos fonctions :

queue.front()();

Et tout l'intérêt réside ici, nous ne savons pas comment ont été placées nos évènements, mais nous savons que nous pouvons les appeler par la signature void ()

Exemple 2, une fonction à appeler mais en choisissant l'instance

Cette fois ci, nous avons une sorte de liste de récepteurs, pour chaque évènement dans la pile et pour chaque récepteur, on appelle une fonction. Ce principe va nécessiter de préciser au moment de l'appel sur quel objet on veut opérer. Cela est possible grâce aux std::placeholders.

#include <functional>
#include <iostream>
#include <queue>
#include <string>

class Handler {
private:
    std::string m_name;

public:
    Handler(const std::string &name)
        : m_name(name)
    {
    }

    void receive(const std::string &data)
    {
        std::cout << "I'm " << m_name << std::endl;
        std::cout << "I received: " << data << std::endl;
    }

    void requestmove(int x, int y)
    {
        std::cout << "I'm " << m_name << std::endl;
        std::cout << "Request moving to " << x << ", " << y << std::endl;
    }
};

using Call  = std::function<void (Handler &)>;
using Queue = std::queue;

int main() {
    using namespace std::placeholders;

    Queue queue;
    Handler h1("Creator");
    Handler h2("Terminator");

    queue.push(std::bind(&Handler::receive, _1, "hello"));
    queue.push(std::bind(&Handler::requestmove, _1, -19, -84));

    while (queue.size() > 0) {
        queue.front()(h1);
        queue.front()(h2);
        queue.pop();
    }

    return 0;
}

Cette fois là, nous ajoutons deux évènements que nous allons appeler pour h1 et h2. Par rapport à avant, on peut voir que la fonction change de signature :

using Call  = std::function;

C'est à ce moment là qu'il faut penser C et pointeur de fonctions, c'est grâce à ce paramètre que nous allons pouvoir appeler la fonction membre d'une instance précise.

queue.push(std::bind(&Handler::receive, _1, "hello"));
queue.push(std::bind(&Handler::requestmove, _1, -19, -84));

Ici, le _1 désigne un paramètre non appliqué. C'est celui ci que nous allons spécifier au moment de l'appel. On pourrait très bien rajouter des paramètres non appliqués sur la fonction à appeler aussi mais ils nécessiteront d'être spécifié au moment de l'appel.

while (queue.size() > 0) {
   queue.front()(h1);
   queue.front()(h2);
   queue.pop();
}

Au moment de l'exécution, la bonne fonction sera appelé pour la bonne instance ce qui devrait vous donner ce résultat :

I'm Creator I received: hello
I'm Terminator I received: hello
I'm Creator Request moving to -19, -84
I'm Terminator Request moving to -19, -84