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

前端 未结 5 1353
囚心锁ツ
囚心锁ツ 2021-01-30 04:41

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 depen

5条回答
  •  广开言路
    2021-01-30 04:45

    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 
    #include 
    #include 
    #include 
    #include 
    #include "Utility/scopeGuard.hpp"
    
    namespace MV {
    
        template 
        class Signal {
        public:
            typedef std::function FunctionType;
            typedef std::shared_ptr> SharedType;
    
            static std::shared_ptr< Signal > make(std::function a_callback){
                return std::shared_ptr< Signal >(new Signal(a_callback, ++uniqueId));
            }
    
            template 
            void notify(Arg... a_parameters){
                if(!isBlocked){
                    callback(std::forward(a_parameters)...);
                }
            }
            template 
            void operator()(Arg... a_parameters){
                if(!isBlocked){
                    callback(std::forward(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& a_rhs){
                return id < a_rhs.id;
            }
            bool operator>(const Signal& a_rhs){
                return id > a_rhs.id;
            }
            bool operator==(const Signal& a_rhs){
                return id == a_rhs.id;
            }
            bool operator!=(const Signal& a_rhs){
                return id != a_rhs.id;
            }
    
        private:
            Signal(std::function 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 
        long long Signal::uniqueId = 0;
    
        template 
        class Slot {
        public:
            typedef std::function FunctionType;
            typedef Signal SignalType;
            typedef std::shared_ptr> SharedSignalType;
    
            //No protection against duplicates.
            std::shared_ptr> connect(std::function a_callback){
                if(observerLimit == std::numeric_limits::max() || cullDeadObservers() < observerLimit){
                    auto signal = Signal::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> a_value){
                if(observerLimit == std::numeric_limits::max() || cullDeadObservers() < observerLimit){
                    observers.insert(a_value);
                    return true;
                }else{
                    return false;
                }
            }
    
            void disconnect(std::shared_ptr> a_value){
                if(!inCall){
                    observers.erase(a_value);
                } else{
                    disconnectQueue.push_back(a_value);
                }
            }
    
            template 
            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(a_parameters)...);
                        i = next;
                    }
                }
            }
    
            void setObserverLimit(size_t a_newLimit){
                observerLimit = a_newLimit;
            }
            void clearObserverLimit(){
                observerLimit = std::numeric_limits::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 >, std::owner_less>> > observers;
            size_t observerLimit = std::numeric_limits::max();
            bool inCall = false;
            std::vector< std::shared_ptr> > 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 
        class SlotRegister {
        public:
            typedef std::function FunctionType;
            typedef Signal SignalType;
            typedef std::shared_ptr> SharedSignalType;
    
            SlotRegister(Slot &a_slot) :
                slot(a_slot){
            }
    
            //no protection against duplicates
            std::shared_ptr> connect(std::function a_callback){
                return slot.connect(a_callback);
            }
            //duplicate shared_ptr's will not be added
            bool connect(std::shared_ptr> a_value){
                return slot.connect(a_value);
            }
    
            void disconnect(std::shared_ptr> a_value){
                slot.disconnect(a_value);
            }
        private:
            Slot &slot;
        };
    
    }
    
    #endif
    

    Supplimental scopeGuard.hpp:

    #ifndef _MV_SCOPEGUARD_H_
    #define _MV_SCOPEGUARD_H_
    
    //Lifted from Alexandrescu's ScopeGuard11 talk.
    
    namespace MV {
        template 
        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
        ScopeGuard scopeGuard(Fun f){
            return ScopeGuard(std::move(f));
        }
    
        namespace ScopeMacroSupport {
            enum class ScopeGuardOnExit {};
            template 
            MV::ScopeGuard operator+(ScopeGuardOnExit, Fun&& fn) {
                return MV::ScopeGuard(std::forward(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 
    #include 
    #include "signal.hpp"
    
    class Observed {
    private:
        //Note: This is private to ensure not just anyone can spawn a signal
        MV::Slot onChangeSlot;
    public:
        typedef MV::Slot::SharedSignalType ChangeEventSignal;
    
        //SlotRegister is public, users can hook up signals to onChange with this value.
        MV::SlotRegister 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.

提交回复
热议问题