C++11 observer pattern (signals, slots, events, change broadcaster/listener, or whatever you want to call it)

20,978

Solution 1

I think that bind makes it easier to create slots (cfr. the 'preferred' syntax vs. the 'portable' syntax - that's all going away). The observer management, however, is not becoming less complex.

But as @R. Martinho Fernandes mentions: an std::vector<std::function< r(a1) > > is now easily created without the hassle for an (artificial) 'pure virtual' interface class.


Upon request: an idea on connection management - probably full of bugs, but you'll get the idea:

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

Solution 2

Since you're asking for code, my blog entry Performance of a C++11 Signal System contains a single-file implementation of a fully functional signal system based on C++11 features without further dependencies (albeit single-threaded, which was a performance requirement).

Here is a brief usage example:

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

More examples can be found in this unit test.

Solution 3

I wrote my own light weight Signal/Slot classes which return connection handles. The existing answer's key system is pretty fragile in the face of exceptions. You have to be exceptionally careful about deleting things with an explicit call. I much prefer using RAII for open/close pairs.

One notable lack of support in my library is the ability to get a return value from your calls. I believe boost::signal has methods of calculating the aggregate return values. In practice usually you don't need this and I just find it cluttering, but I may come up with such a return method for fun as an exercise in the future.

One cool thing about my classes is the Slot and SlotRegister classes. SlotRegister provides a public interface which you can safely link to a private Slot. This protects against external objects calling your observer methods. It's simple, but nice encapsulation.

I do not believe my code is thread safe, however.

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

An example application making use of my library:

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

Output of the above would be:

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

As you can see, the slot disconnects dead signals automatically.

Solution 4

Here's what I came up with.

This assumes no need to aggregate results from the listeners of a broadcast signal. Also, the "slot" or Signal::Listener is the owner of the callback. This ought to live with the object that your (I'm guessing...) lambda is probably capturing so that when that object goes out of scope, so does the callback, which prevents it from being called anymore.

You could use methods described in other answers as well to store the Listener owner objects in a way you can lookup.

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

I have made a gist for this too, with a more in-depth example: https://gist.github.com/johnb003/dbc4a69af8ea8f4771666ce2e383047d

Share:
20,978
learnvst
Author by

learnvst

I like sound

Updated on October 11, 2020

Comments

  • learnvst
    learnvst over 3 years

    With the changes made in C++11 (such as the inclusion of std::bind), is there a recommended way to implement a simple single-threaded observer pattern without dependence on anything external to the core language or standard library (like boost::signal)?

    EDIT

    If someone could post some code showing how dependence on boost::signal could be reduced using new language features, that would still be very useful.