web-dev-qa-db-fra.com

Modèle d'observateur C ++ 11 (signaux, créneaux, événements, changement de diffuseur / auditeur, ou quel que soit le nom que vous souhaitez utiliser)

Avec les modifications apportées en C++ 11 (telles que l'inclusion de std::bind), existe-t-il un moyen recommandé d'implémenter un modèle d'observateur simple à thread unique sans dépendre de quelque chose d'extérieur au langage de base ou à la bibliothèque standard (comme boost::signal)?

MODIFIER

Si quelqu'un pouvait publier du code montrant comment la dépendance à boost::signal pourrait être réduit en utilisant de nouvelles fonctionnalités linguistiques, ce qui serait toujours très utile.

34
learnvst

Je pense que bind facilite la création de slots (cfr. La syntaxe "préférée" vs "portable") 'syntax - tout cela s'en va). Cependant, la gestion des observateurs ne devient pas moins complexe.

Mais comme @R. Martinho Fernandes mentionne: une std::vector<std::function< r(a1) > > est maintenant facilement créée sans les tracas d'une classe d'interface (artificielle) 'pure virtuelle'.


Sur demande: une idée sur la gestion des connexions - probablement pleine de bugs, mais vous aurez l'idée:

// note that the Func parameter is something
// like std::function< void(int,int) > or whatever, greatly simplified
// by the C++11 standard
template<typename Func>
struct signal {
  typedef int Key; // 
  Key nextKey;
  std::map<Key,Func> connections;

  // note that connection management is the same in C++03 or C++11
  // (until a better idea arises)
  template<typename FuncLike>
  Key connect( FuncLike f ) {
     Key k=nextKey++;
     connections[k]=f;
     return k;
  }

  void disconnect(Key k){
     connections.erase(k);
  }

  // note: variadic template syntax to be reviewed 
  // (not the main focus of this post)
  template<typename Args...>
  typename Func::return_value call(Args... args){
     // supposing no subcription changes within call:
     for(auto &connection: connections){
        (*connection.second)(std::forward(...args));
     }
  }
};

Usage:

signal<function<void(int,int)>> xychanged;

void dump(int x, int y) { cout << x << ", " << y << endl; }

struct XY { int x, y; } xy;

auto dumpkey=xychanged.connect(dump);
auto lambdakey=xychanged.connect([&xy](int x, int y){ xy.x=x; xy.y=y; });

xychanged.call(1,2);
28
xtofl

Puisque vous demandez du code, mon entrée de blog Performance of a C++ 11 Signal System contient une implémentation à fichier unique d'un système de signal entièrement fonctionnel basé sur les fonctionnalités de C++ 11 sans autres dépendances ( quoique monothread, ce qui était une exigence de performance).

Voici un bref exemple d'utilisation:

Signal<void (std::string, int)> sig2;
sig2() += [] (std::string msg, int d)   { /* handler logic */ };
sig2.emit ("string arg", 17);

Plus d'exemples peuvent être trouvés dans ce test unitaire .

3
TimJ

J'ai écrit mes propres classes Signal/Slot légères qui renvoient des poignées de connexion. Le système de clé de réponse existant est assez fragile face aux exceptions. Vous devez être extrêmement prudent lors de la suppression de choses avec un appel explicite. Je préfère de loin utiliser RAII pour les paires ouvertes/fermées.

Un manque notable de support dans ma bibliothèque est la possibilité d'obtenir une valeur de retour de vos appels. Je crois que boost :: signal a des méthodes de calcul des valeurs de retour agrégées. Dans la pratique, vous n'avez généralement pas besoin de cela et je trouve cela encombrant, mais je peux trouver une telle méthode de retour pour le plaisir comme exercice à l'avenir.

Une chose intéressante à propos de mes classes est les classes Slot et SlotRegister. SlotRegister fournit une interface publique que vous pouvez lier en toute sécurité à un slot privé. Cela protège contre les objets externes appelant vos méthodes d'observation. C'est simple, mais une belle encapsulation.

Cependant, je ne pense pas que mon code soit thread-safe.

//"MIT License + do not delete this comment" - M2tM : http://michaelhamilton.com 

#ifndef __MV_SIGNAL_H__
#define __MV_SIGNAL_H__

#include <memory>
#include <utility>
#include <functional>
#include <vector>
#include <set>
#include "Utility/scopeGuard.hpp"

namespace MV {

    template <typename T>
    class Signal {
    public:
        typedef std::function<T> FunctionType;
        typedef std::shared_ptr<Signal<T>> SharedType;

        static std::shared_ptr< Signal<T> > make(std::function<T> a_callback){
            return std::shared_ptr< Signal<T> >(new Signal<T>(a_callback, ++uniqueId));
        }

        template <class ...Arg>
        void notify(Arg... a_parameters){
            if(!isBlocked){
                callback(std::forward<Arg>(a_parameters)...);
            }
        }
        template <class ...Arg>
        void operator()(Arg... a_parameters){
            if(!isBlocked){
                callback(std::forward<Arg>(a_parameters)...);
            }
        }

        void block(){
            isBlocked = true;
        }
        void unblock(){
            isBlocked = false;
        }
        bool blocked() const{
            return isBlocked;
        }

        //For sorting and comparison (removal/avoiding duplicates)
        bool operator<(const Signal<T>& a_rhs){
            return id < a_rhs.id;
        }
        bool operator>(const Signal<T>& a_rhs){
            return id > a_rhs.id;
        }
        bool operator==(const Signal<T>& a_rhs){
            return id == a_rhs.id;
        }
        bool operator!=(const Signal<T>& a_rhs){
            return id != a_rhs.id;
        }

    private:
        Signal(std::function<T> a_callback, long long a_id):
            id(a_id),
            callback(a_callback),
            isBlocked(false){
        }
        bool isBlocked;
        std::function< T > callback;
        long long id;
        static long long uniqueId;
    };

    template <typename T>
    long long Signal<T>::uniqueId = 0;

    template <typename T>
    class Slot {
    public:
        typedef std::function<T> FunctionType;
        typedef Signal<T> SignalType;
        typedef std::shared_ptr<Signal<T>> SharedSignalType;

        //No protection against duplicates.
        std::shared_ptr<Signal<T>> connect(std::function<T> a_callback){
            if(observerLimit == std::numeric_limits<size_t>::max() || cullDeadObservers() < observerLimit){
                auto signal = Signal<T>::make(a_callback);
                observers.insert(signal);
                return signal;
            } else{
                return nullptr;
            }
        }
        //Duplicate Signals will not be added. If std::function ever becomes comparable this can all be much safer.
        bool connect(std::shared_ptr<Signal<T>> a_value){
            if(observerLimit == std::numeric_limits<size_t>::max() || cullDeadObservers() < observerLimit){
                observers.insert(a_value);
                return true;
            }else{
                return false;
            }
        }

        void disconnect(std::shared_ptr<Signal<T>> a_value){
            if(!inCall){
                observers.erase(a_value);
            } else{
                disconnectQueue.Push_back(a_value);
            }
        }

        template <typename ...Arg>
        void operator()(Arg... a_parameters){
            inCall = true;
            SCOPE_EXIT{
                inCall = false;
                for(auto& i : disconnectQueue){
                    observers.erase(i);
                }
                disconnectQueue.clear();
            };

            for (auto i = observers.begin(); i != observers.end();) {
                if (i->expired()) {
                    observers.erase(i++);
                } else {
                    auto next = i;
                    ++next;
                    i->lock()->notify(std::forward<Arg>(a_parameters)...);
                    i = next;
                }
            }
        }

        void setObserverLimit(size_t a_newLimit){
            observerLimit = a_newLimit;
        }
        void clearObserverLimit(){
            observerLimit = std::numeric_limits<size_t>::max();
        }
        int getObserverLimit(){
            return observerLimit;
        }

        size_t cullDeadObservers(){
            for(auto i = observers.begin(); i != observers.end();) {
                if(i->expired()) {
                    observers.erase(i++);
                }
            }
            return observers.size();
        }
    private:
        std::set< std::weak_ptr< Signal<T> >, std::owner_less<std::weak_ptr<Signal<T>>> > observers;
        size_t observerLimit = std::numeric_limits<size_t>::max();
        bool inCall = false;
        std::vector< std::shared_ptr<Signal<T>> > disconnectQueue;
    };

    //Can be used as a public SlotRegister member for connecting slots to a private Slot member.
    //In this way you won't have to write forwarding connect/disconnect boilerplate for your classes.
    template <typename T>
    class SlotRegister {
    public:
        typedef std::function<T> FunctionType;
        typedef Signal<T> SignalType;
        typedef std::shared_ptr<Signal<T>> SharedSignalType;

        SlotRegister(Slot<T> &a_slot) :
            slot(a_slot){
        }

        //no protection against duplicates
        std::shared_ptr<Signal<T>> connect(std::function<T> a_callback){
            return slot.connect(a_callback);
        }
        //duplicate shared_ptr's will not be added
        bool connect(std::shared_ptr<Signal<T>> a_value){
            return slot.connect(a_value);
        }

        void disconnect(std::shared_ptr<Signal<T>> a_value){
            slot.disconnect(a_value);
        }
    private:
        Slot<T> &slot;
    };

}

#endif

Scope supplémentaireGuard.hpp:

#ifndef _MV_SCOPEGUARD_H_
#define _MV_SCOPEGUARD_H_

//Lifted from Alexandrescu's ScopeGuard11 talk.

namespace MV {
    template <typename Fun>
    class ScopeGuard {
        Fun f_;
        bool active_;
    public:
        ScopeGuard(Fun f)
            : f_(std::move(f))
            , active_(true) {
        }
        ~ScopeGuard() { if(active_) f_(); }
        void dismiss() { active_ = false; }
        ScopeGuard() = delete;
        ScopeGuard(const ScopeGuard&) = delete;
        ScopeGuard& operator=(const ScopeGuard&) = delete;
        ScopeGuard(ScopeGuard&& rhs)
            : f_(std::move(rhs.f_))
            , active_(rhs.active_) {
            rhs.dismiss();
        }
    };

    template<typename Fun>
    ScopeGuard<Fun> scopeGuard(Fun f){
        return ScopeGuard<Fun>(std::move(f));
    }

    namespace ScopeMacroSupport {
        enum class ScopeGuardOnExit {};
        template <typename Fun>
        MV::ScopeGuard<Fun> operator+(ScopeGuardOnExit, Fun&& fn) {
            return MV::ScopeGuard<Fun>(std::forward<Fun>(fn));
        }
    }

#define SCOPE_EXIT \
    auto ANONYMOUS_VARIABLE(SCOPE_EXIT_STATE) \
    = MV::ScopeMacroSupport::ScopeGuardOnExit() + [&]()

#define CONCATENATE_IMPL(s1, s2) s1##s2
#define CONCATENATE(s1, s2) CONCATENATE_IMPL(s1, s2)
#ifdef __COUNTER__
#define ANONYMOUS_VARIABLE(str) \
    CONCATENATE(str, __COUNTER__)
#else
#define ANONYMOUS_VARIABLE(str) \
    CONCATENATE(str, __LINE__)
#endif
}

#endif

Un exemple d'application utilisant ma bibliothèque:

#include <iostream>
#include <string>
#include "signal.hpp"

class Observed {
private:
    //Note: This is private to ensure not just anyone can spawn a signal
    MV::Slot<void (int)> onChangeSlot;
public:
    typedef MV::Slot<void (int)>::SharedSignalType ChangeEventSignal;

    //SlotRegister is public, users can hook up signals to onChange with this value.
    MV::SlotRegister<void (int)> onChange;

    Observed():
        onChange(onChangeSlot){ //Here is where the binding occurs
    }

    void change(int newValue){
        onChangeSlot(newValue);
    }
};

class Observer{
public:
    Observer(std::string a_name, Observed &a_observed){
        connection = a_observed.onChange.connect([=](int value){
            std::cout << a_name << " caught changed value: " << value << std::endl;
        });
    }
private:
    Observed::ChangeEventSignal connection;
};

int main(){
    Observed observed;
    Observer observer1("o[1]", observed);
    {
        Observer observer2("o[2]", observed);
        observed.change(1);
    }
    observed.change(2);
}

Le résultat de ce qui précède serait:

o[1] caught changed value: 1
o[2] caught changed value: 1
o[1] caught changed value: 2

Comme vous pouvez le voir, le slot déconnecte automatiquement les signaux morts.

2
M2tM

Voici ce que j'ai trouvé.

Cela suppose qu'il n'est pas nécessaire d'agréger les résultats des auditeurs d'un signal diffusé. En outre, le "slot" ou Signal :: Listener est le propriétaire du rappel. Cela devrait vivre avec l'objet que votre (je suppose ...) lambda est probablement en train de capturer de sorte que lorsque cet objet est hors de portée, le rappel aussi, ce qui l'empêche d'être appelé plus.

Vous pouvez également utiliser les méthodes décrites dans d'autres réponses pour stocker les objets propriétaires du récepteur d'une manière que vous pouvez rechercher.

template <typename... FuncArgs>
class Signal
{
    using fp = std::function<void(FuncArgs...)>;
    std::forward_list<std::weak_ptr<fp> > registeredListeners;
public:
    using Listener = std::shared_ptr<fp>;

    Listener add(const std::function<void(FuncArgs...)> &cb) {
        // passing by address, until copy is made in the Listener as owner.
        Listener result(std::make_shared<fp>(cb));
        registeredListeners.Push_front(result);
        return result;
    }

    void raise(FuncArgs... args) {
        registeredListeners.remove_if([&args...](std::weak_ptr<fp> e) -> bool {
            if (auto f = e.lock()) {
                (*f)(args...);
                return false;
            }
            return true;
        });
    }
};

usage

Signal<int> bloopChanged;

// ...

Signal<int>::Listener bloopResponse = bloopChanged.add([](int i) { ... });
// or
decltype(bloopChanged)::Listener bloopResponse = ...

// let bloopResponse go out of scope.
// or re-assign it
// or reset the shared_ptr to disconnect it
bloopResponse.reset();

J'ai également créé un Gist pour cela, avec un exemple plus détaillé: https://Gist.github.com/johnb003/dbc4a69af8ea8f4771666ce2e383047d

0
johnb003

J'ai moi-même essayé cela. Mes efforts se trouvent à ce Gist, qui continuera d'évoluer. . .

https://Gist.github.com/4172757

J'utilise un style différent, plus similaire aux notifications de changement dans JUCE qu'aux signaux BOOST. La gestion des connexions se fait à l'aide d'une syntaxe lambda qui effectue une capture par copie. Jusqu'à présent, cela fonctionne bien.

0
learnvst