web-dev-qa-db-ja.com

C ++ 11オブザーバーパターン(シグナル、スロット、イベント、変更ブロードキャスター/リスナー、または呼び出したいもの)

C++ 11で行われた変更(std::bindの追加など)により、コア言語または標準ライブラリの外部に依存することなく、単純なシングルスレッドのオブザーバーパターンを実装する推奨方法があります( boost::signal)?

[〜#〜]編集[〜#〜]

誰かが新しい言語機能を使用してboost::signalへの依存をどのように減らすことができるかを示すコードを投稿できたとしても、それは非常に便利です。

34
learnvst

bindはスロットを作成するのに簡単になると思います(参照: 'preferred' syntax vs. '構文 -それで終わりです)。ただし、オブザーバー管理はそれほど複雑になりません。

しかし@Rとして。 Martinho Fernandesは次のように述べています: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);
28
xtofl

あなたがコードを求めているので、私のブログエントリ C++ 11信号システムのパフォーマンス には、C++ 11機能に基づく完全に機能する信号システムの単一ファイルの実装が含まれています。シングルスレッドですが、これはパフォーマンス要件です)。

以下に簡単な使用例を示します。

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

その他の例は、この 単体テスト にあります。

3
TimJ

接続ハンドルを返す独自の軽量のSignal/Slotクラスを作成しました。既存の回答の主要なシステムは、例外に直面してかなり壊れやすいです。明示的な呼び出しによるものの削除には、非常に注意が必要です。私は [〜#〜] raii [〜#〜] をオープン/クローズのペアに使用することを非常に好みます。

私のライブラリでのサポートの欠如の1つは、呼び出しから戻り値を取得する機能です。 boost :: signalには、総戻り値を計算するメソッドがあると思います。実際には通常これは必要ありませんが、散らかっていますが、将来の練習として、楽しみのためにこのようなreturnメソッドを考え出すかもしれません。

私のクラスのすばらしい点の1つは、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

追加のscopeGuard.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

私のライブラリを使用するサンプルアプリケーション:

#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

ご覧のように、スロットはデッド信号を自動的に切断します。

2
M2tM

これが私が思いついたものです。

これは、ブロードキャスト信号のリスナーからの結果を集約する必要がないことを前提としています。また、「スロット」またはSignal :: Listenerは、コールバックの所有者です。これは、あなたの(私が推測している...)ラムダがキャプチャしているオブジェクトと共存する必要があるため、そのオブジェクトがスコープから外れると、コールバックも行われ、コールバックが行われなくなります。

他の回答で説明されているメソッドを使用して、リスナーの所有者オブジェクトをルックアップできる方法で格納することもできます。

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

使用法

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

私はこれについても詳細な例を挙げて要点を作りました: https://Gist.github.com/johnb003/dbc4a69af8ea8f4771666ce2e383047d

0
johnb003

私もこれをやってみました。私の努力はこのGistで見つけることができます。 。 。

https://Gist.github.com/4172757

私は、BOOSTシグナルよりもJUCEの変更通知に似た別のスタイルを使用しています。接続管理は、コピーによるキャプチャを行うラムダ構文を使用して行われます。今のところ順調です。

0
learnvst