Шаблон наблюдателя C++11 (сигналы, слоты, события, изменение вещателя / слушателя или как вы хотите его назвать)

С изменениями, внесенными в C++11 (например, включение std::bind), есть ли рекомендуемый способ реализовать простой однопоточный шаблон наблюдателя без зависимости от чего-либо внешнего по отношению к основному языку или стандартной библиотеке (например,boost::signal)?

редактировать

Если кто-то может пост какой-то код, показывающий, как зависимость boost::signal можно было бы уменьшить с помощью новых языковых функций, что все равно было бы очень полезно.

4 ответов


думаю, что bind делает легче для создания слотов (cfr. the "предпочтительный" синтаксис против "портативного" синтаксиса - это все девается). Однако управление наблюдателями не становится менее сложным.

но как упоминает @R. Martinho Fernandes: an std::vector<std::function< r(a1) > > теперь легко создается без хлопот для (искусственного) "чистого виртуального" класса интерфейса.


по запросу: идея по управлению соединением-вероятно, полная ошибки, но вы получите идею:

// 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));
     }
  }
};

использование:

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);

Я написал свои собственные облегченные классы сигнала / слота которые возвращают ручки соединения. Ключевая система существующего ответа довольно хрупка перед лицом исключений. Вы должны быть исключительно осторожны при удалении вещей с явным вызовом. Я предпочитаю использовать RAII для пар открытия / закрытия.

одним из заметных недостатков поддержки в моей библиотеке является возможность получить возвращаемое значение от ваших вызовов. Я считаю, что boost:: signal имеет методы расчета агрегата значение. На практике обычно вам это не нужно, и я просто нахожу это беспорядочным, но я могу придумать такой метод возврата для удовольствия, как упражнение в будущем.

одна классная вещь о моих классах-это классы Slot и SlotRegister. SlotRegister предоставляет открытый интерфейс, который можно безопасно связать с частным слотом. Это защищает от внешних объектов, вызывающих методы наблюдателя. Это простая, но хорошая инкапсуляция.

Я не верю, что мой код однако потокобезопасно.

//"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

Supplimental scopeGuard.ГЭС:

#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

пример приложения, использующего мою библиотеку:

#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);
}

выход выше будет:

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

как вы можете видеть, слот автоматически отключает мертвые сигналы.


Так как вы просите код, Моя запись в блоге работу с 11 система++сигнал содержит однофайловую реализацию полнофункциональной сигнальной системы на основе функций c++11 без дополнительных зависимостей (хотя и однопоточную, что было требованием к производительности).

вот краткий пример использования:

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

дополнительные примеры можно найти в этом единица теста.


Я тоже попробовал это сделать. Мои усилия можно найти в этой сути, которая будет продолжать развиваться . . .

https://gist.github.com/4172757

Я использую другой стиль, более похожий на уведомления об изменениях в JUCE, чем сигналы BOOST. Управление соединениями выполняется с использованием некоторого лямбда-синтаксиса, который делает некоторый захват копией. Пока он работает хорошо.