5
\$\begingroup\$

Implemented observers in C++11 in a generic and statically accessible way. Though an instance is created, it's used only for cleanup and lifetime management.

template<typename... Args>
class Observers
{
private:
    unordered_map<int, vector<function<void(Args...)>>> observers;
    static Observers<Args...> *instance;
public:
    Observers()
    {
        instance = this;
    }

    template<typename Observer>
    static void Register(int ev, Observer &&observer)
    {
        instance->observers[ev].push_back(forward<Observer>(observer));
    }

    static void Notify(int ev, Args... args)
    {
        try
        {
            auto &obs = instance->observers.at(ev);
            for (auto &o : obs)
                o(args...);
        }
        catch (...){}
    }
};

template<typename... Args>
Observers<Args...> *Observers<Args...>::instance;

usage:

using IntIntObservers = Observers < int, int > ;//optional type declaration

IntIntObservers o;//observer instance

IntIntObservers::Register(1, [=](int a, int b){whatever});

IntIntObservers::Notify(1, 4, 5);

An observer set can be accessed statically as long as the instance is alive. Multiple instances of the same type could be achieved by adding an id parameter to the template declaration, making it

template<int id, typename... Args>

Anything that I missed and could be improved?

\$\endgroup\$
2
\$\begingroup\$

Briefly, I find the following:

  • The current static behaviour is not intuitive (what does int ev really mean? why numbers and not names?), not efficient (why search in an unordered map rather that direct access?) and not thread-safe (instance is shared among all threads).

  • A list might be more appropriate to store observers of one event, enabling easier management like removing an observer.

  • Catching all exceptions in such a generic tool is a limitation. If needed, it could be at least parametrized.

  • Parameter pack Args... does not take into account return types.

  • Your Notify takes its arguments by value. Forwarding is not appropriate when calling multiple functions, but at least arguments should be passed by reference.

At a very basic level, I would prefer an object, called e.g. signal, templated on full function signature e.g. R(A...), with non-static members:

  • (private) observers: std::list or some associative container of std::function<R(A...)>

  • add()/remove() or connect()/disconnect() to manage the content of observers. remove() would need some form of identification of existing observers, hence the need for an associative container.

  • operator()(A&&... a) so that signal behaves like a function object. Arguments a... are passed without forwarding to underlying functions, so that they are copied if needed, and not invalidated between subsequent calls.

  • additional management like clear()/empty().

Your example usage would be rather like

signal<void(int, int)> s;
s.connect([=](int a, int b){whatever});
s(4, 5);

Isn't that better? Since everything is non-static, the user controls how many instances there are, and names them properly. In fact, a single object may have e.g. ten methods, each with its own observers. Each such method is then a signal and behaves like a function object.

Additionally

  • std::function expects the 1st argument of a call to be the object if bound on a member function. It would be much more convenient to have functionality such that the object is stored separately and the signal's signature matches exactly that of the member function. In many applications everything is about objects + member functions rather than free functions.

  • Though non-trivial, possibly provide a mechanism to collect return values of called (observer) functions plus policies to choose, compute and return a single return value from operator().

Have a look at boost::signals and boost::signals2 for more ideas.

I have implemented my own model in the past, using delegates instead of std::function. There is an amazing amount of additional work if such "signals" are to fully mimic functions or member functions, including:

  • default arguments (arbitrary values encoded into types in the signature itself)
  • overloading (with different signatures)
  • inheritance (overriding member functions in base/derived classes)
  • virtual functions
  • ...

I have used it as a full C++-compliant replacement for Qt signals/slots and wrapped most of the Qt Gui module so that a project can be built without moc and without qmake. One day I will hopefully clean and rewrite in C++11.

\$\endgroup\$
  • \$\begingroup\$ In my experience, std::list is slower than std::vector, even when complexity says otherwise. Arrays are just that much faster on C++. The event type is an int because I felt like using enums, it could be a templated type. \$\endgroup\$ – DariusL May 7 '14 at 10:00
  • \$\begingroup\$ @user1560102 std::list: depends on usage. Event "types": ok, let us put all our double variables of a program into a static unordered_map<int, double> called DoubleVariables and use a number instead of a name to Register or Get each variable as an entry in this map. How does this look? To me, it looks like assembly. \$\endgroup\$ – iavr May 7 '14 at 10:54

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.