34

随着 C++11 中所做的更改(例如包含std::bind),是否有推荐的方法来实现简单的单线程观察者模式,而不依赖于核心语言或标准库(如boost::signal)之外的任何东西?

编辑

如果有人可以发布一些代码来展示如何使用新的语言特性来减少依赖boost::signal,那仍然非常有用。

4

5 回答 5

28

我认为这bind使得创建插槽变得更容易(参见“首选”语法与“便携式”语法——这一切都将消失)。然而,观察员管理并没有变得不那么复杂。

但作为@R。Martinho Fernandes 提到:std::vector<std::function< r(a1) > >现在可以轻松创建 an ,而无需(人工)“纯虚拟”接口类。


根据要求:关于连接管理的想法 - 可能充满了错误,但你会明白的:

// 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);
于 2012-11-27T21:01:16.120 回答
3

由于您要代码,我的博客条目C++11 信号系统的性能包含基于 C++11 功能的全功能信号系统的单文件实现,没有进一步的依赖关系(尽管是单线程的,它是性能要求)。

下面是一个简短的使用示例:

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

更多示例可以在这个单元测试中找到。

于 2014-10-03T03:43:15.197 回答
2

我编写了自己的轻量级 Signal/Slot 类,它们返回连接句柄。面对异常,现有答案的关键系统非常脆弱。您必须格外小心地使用显式调用删除内容。我更喜欢将RAII用于开/关对。

我的库中一个值得注意的缺乏支持是无法从您的调用中获取返回值。我相信 boost::signal 有计算总返回值的方法。在实践中通常你不需要这个,我只是觉得它很杂乱,但我可能会想出这样一个返回方法来娱乐作为未来的练习。

关于我的课程的一件很酷的事情是 Slot 和 SlotRegister 课程。SlotRegister 提供了一个公共接口,您可以安全地链接到私有 Slot。这可以防止外部对象调用您的观察者方法。它很简单,但封装很好。

但是,我不相信我的代码是线程安全的。

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

如您所见,插槽会自动断开死信号。

于 2014-04-04T22:25:26.373 回答
1

这就是我想出的。

这假设不需要从广播信号的侦听器聚合结果。此外,“插槽”或​​ Signal::Listener 是回调的所有者。这应该与您的(我猜......)lambda可能正在捕获的对象一起使用,以便当该对象超出范围时,回调也将阻止它被调用。

您也可以使用其他答案中描述的方法以您可以查找的方式存储侦听器所有者对象。

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

于 2019-10-10T21:12:28.400 回答
0

我自己也尝试过。我的努力可以在这个 gist 中找到,它将继续发展。. .

https://gist.github.com/4172757

我使用了不同的风格,更类似于 JUCE 中的更改通知,而不是 BOOST 信号。连接管理是使用一些 lambda 语法完成的,该语法通过复制进行一些捕获。到目前为止,它运行良好。

于 2012-11-30T03:30:06.853 回答