3
\$\begingroup\$

I have been playing around with ways of implementing a generic class for supporting the observer pattern. I have already asked to review a basic approach here. Now I have come up with an Observable class that supports parametrizing with an arbitrary number of function signatures, in order for the subject class to register and notify observers interested in different kinds of events that require different kind of information to be transmitted.

Here is the code:

observable.h:

#include <functional>
#include <vector>
#include <tuple>

template<typename ... Funcs>
class Observable
{
public:
    using ObserverList = std::tuple<std::vector<std::function<Funcs>>...>;

    template<unsigned int eventidx, typename Callable>
    void AttachObserver(Callable c)
    {
        std::get<eventidx>(observers).push_back(c);
    }

    template<unsigned int eventidx, typename ... T>
    void NotifyObserversOfEvent(T ... args)
    {
        for(auto f: std::get<eventidx>(observers))
        {
            f(std::forward<T>(args)...);
        }
    }

private:
    ObserverList observers;
};

Sample Test code

main.cpp:

#include <iostream>
#include <functional>
#include <vector>

#include "observable.h"

class A : public Observable<
                            void(void),
                            void(int,std::string),
                            void(int,int),
                            >
{
public:
    enum Event : unsigned
    {
        Event0,
        Event1,
        Event2
    };

private:
    int a;
    std::string s;
};

class B
{
public:
    void EventZeroHappened()
    {
        std::cout << "B has observed event zero\n";
    }


    void EventOneHappened(int a, std::string s)
    {
        std::cout << "B has observed event one with params: " << a << " and " << "\"" << s << "\"" <<'\n';
    }

    void EventTwoHappened(int a, int b)
    {
        std::cout << "B has observed event two with params: " << a << " and " << b <<'\n';
    }
};




int main()
{
    A a;
    B b;

    a.AttachObserver<A::Event::Event0>([&b](){b.EventZeroHappened();});
    a.AttachObserver<A::Event::Event1>([&b](int i, std::string s){b.EventOneHappened(i,s);});
    a.AttachObserver<A::Event::Event2>([&b](int i, int j){b.EventTwoHappened(i,j);});

    a.NotifyObserversOfEvent<A::Event::Event0>();
    a.NotifyObserversOfEvent<A::Event::Event1>(37,"Hello There");
    a.NotifyObserversOfEvent<A::Event::Event2>(182,150);
}

Basically the idea I wanted to implement is that the user of the observable class, simply writes in the template parameter list, the function signatures of the callback it supports. Instead of forcing the observer to implement an abstract interface and derive from it, which pollutes the observer code and creates an unnecessary dependency. The only draw back is that in order to select which event the observer wants to subscribe to (and which event the subject wants to transmit) it has to be done via integral constant, which is made a bit nicer in the example with an enum. I'm still searching for a better way to do this part.

The reason behind that is that, even in the old way of implementing observer, where you hard-code in a *.h file a pure abstract class and tell other classes that want to observe: "please overload this virtual functions if you want to observe me". And the meaning of each of the events you are going to notify is encoded in the names of the functions. I thought this example was a more general and flexible way of transmitting the same information. Plus being a bit easier to modify to add new events.

As always, I would welcome any comments, corrections criticisms and suggestions.

\$\endgroup\$
1
\$\begingroup\$

There is a way to get rid of the integral constant dispatch without forcing the user to inherit from an observable class, but it's a bit complex.

The idea is to encapsulate runtime polymorphism inside the Observable class. The pattern looks like:

class PolymorphicBehavior {
    class Concept {
        // here are the virtual functions
    };
    template <typename ConcreteType>
    class Object {
        // here are concrete implementations of the functions
        // The class encapsulates type information
        private:
        ConcreteType value;
    };
    public: 
    template <typename ConcreteType>
    PolymorphicBehavior(const ConcreteType& ct) : ptr(new Object<ConcreteType>(ct)) {}
    private:
    Concept* ptr;
};

In the case of Observable, this pattern allows you to erase the function signature (concrete type information is stored in the inner object). There's still another type erasure to perform though, because we'll need to pass arguments to the encapsulated functions. I used std::any over a tuple to create a unified interface.

Here's the full code:

#include <iostream>
#include <vector>
#include <memory>
#include <tuple>
#include <any>
#include <functional>

//deducing the tuple type from the function type
// e.g: std::function<void(float, double)> => std::tuple<float, double>
template <typename T>
struct tuple_desc;
template <typename R, typename... Args>
struct tuple_desc<std::function<R(Args...)>> {
    using type = std::tuple<Args...>;
};

// deducing the function signature from the callable
template <typename Callable>
using function_class = decltype(std::function(std::declval<Callable>()));

class Observable {

    class Observable_model { // type neutral hook
        public:
        virtual void notify(unsigned event_number, std::any& argument_wrapper) = 0;
        virtual ~Observable_model() = default;
    };

    template <typename Callback>
    class Observable_object : public Observable_model { // stores concrete type information
        public:
        Observable_object(unsigned event_class, const Callback& cbk) : event_class(event_class), callback(cbk) {}

        void notify(unsigned event_number, std::any& argument_wrapper) override {
            using Arg_tuple = typename tuple_desc<decltype(this->callback)>::type; // so tuple<int, float> for [](int a, float b) { /*...*/ }
            if (event_number == event_class) { // dynamic dispatch
                std::apply(callback, std::any_cast<Arg_tuple> (argument_wrapper));
            }
        }
        private:
        unsigned event_class;
        function_class<Callback> callback;
    };

    public:
    template <typename Callback>
    void attach(unsigned event_class, Callback&& callback) {
        auto new_obj = std::make_unique<Observable_object<Callback>>(event_class, callback);
        observations.push_back(std::move(new_obj));
    }
    template <typename... Args>
    void notify(unsigned event_number, Args&&... args) {
        std::tuple<Args...> arg_tuple(std::forward<Args>(args)...);
        std::any argument_wrapper(arg_tuple); // argument type erasure
        for (auto& obs : observations) obs->notify(event_number, argument_wrapper);
    }

    private:
    std::vector<std::unique_ptr<Observable_model>> observations;

};

// testing the interface;

enum { WILDLIFE = 0, MACHINES = 1 };

struct Buzzer {
    void go(int h) { std::cout << "drrrr... drrrr... it's " << h << " a.m, time to wake up!\n"; }
};
struct Coffee_machine {
    void make_coffee(int h) { if (h == 7) std::cout << "time to make coffee!\n"; else std::cout << "no coffee yet baby!\n"; }
};

struct Rooster {
    void sing() { std::cout << "Cocorico!\n"; }
};
struct Vampire {
    void bury() { std::cout << "Bye! I'll be in my grave!\n"; }
};

struct Sun : public Observable {
    void rise() {
        std::cout << "Waouh, the sun rises!\n";
        notify(WILDLIFE);
        notify(MACHINES, 6);
    }
};

int main() {   
    Sun sun;
    Buzzer buzzer;
    Rooster rooster;
    Vampire vampire;
    Coffee_machine coffee_machine;
    sun.attach(WILDLIFE, [&rooster] {rooster.sing();});
    sun.attach(WILDLIFE, [&vampire] {vampire.bury();});
    sun.attach(MACHINES, [&buzzer](int h) {buzzer.go(h);});
    sun.attach(MACHINES, [&coffee_machine](int h) {coffee_machine.make_coffee(h);});
    sun.rise(); 
}
\$\endgroup\$
  • \$\begingroup\$ Damn! I'll need some time to read this thoroughly before I can understand what it's doing. Thanks for taking the time to respond! \$\endgroup\$ – bone Jan 16 '18 at 14:20
  • \$\begingroup\$ Edit: So I tried your code, but I changed the last attach to: sun.attach(MACHINES, [&coffee_machine](int h, int i) {coffee_machine.make_coffee(h+i);}); And it fails at runtime with a bad_any_cast exception. My idea was to have the events be tied to a specific function signature at compile time. Your idea is good, but it is not exactly what I am intending. \$\endgroup\$ – bone Jan 16 '18 at 15:35
  • \$\begingroup\$ Sure it fails at runtime. Machines is the equivalent of one of your events and is tied to a signature with only one int. There's a mapping from a run time integer (an enum in my code to be more expressive) to a function signature. \$\endgroup\$ – papagaga Jan 16 '18 at 20:36
  • \$\begingroup\$ Yes I see. IF I understand it correctly, the problem with your code is that the first observer calling attach will bind the event to the signature of its choosing. I wanted the subject class inheriting from Observable to be able to configure the association of event to function signature, and force observers to abide to that, or have compile error if they use it wrong. \$\endgroup\$ – bone Jan 16 '18 at 22:01
  • \$\begingroup\$ The observable class is doing the configuring via the attachmethod. The observers are regular lambdas or functions, nothing fancy which could bind events and signatures. As to compile time errors, you're right, it's a good thing to strive for. It's a question of balance, because on the other hand your interface is quite restrictive (no run-time attaching lacks flexibility) and a bit ugly. I guess you could try and combine both approach in some way . \$\endgroup\$ – papagaga Jan 16 '18 at 23:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.