Search code examples
c++c++11observer-pattern

generic observer pattern


I'm working with code, which has a lot of observer pattern implementations. All of them are organized in such a manner:

Some interface to be implemented by observers:

class ObserverInterface {
  virtual void FooOccurs() = 0;
};

Some class, which implements Register, Unregister and notifications:

class ObservableImpl {
  public:
    Register(ObserverInterface *observer);
    Unregister(ObserverInterface *observer);

  private:
    void SomeMethod() {
      // foo things
      for(auto &observer: observers) {
        observer.FooOccurs();
      }
    }
};

Every time there is a copy-paste of Register and Unregister as well as implementation of notification for each method of ObserverInterface. And every time a programmer has to remember about calling Unregister(), if its observer is going to be destructed.

I wish to enclose the observer pattern in two class templates. So far I've got something like that: http://rextester.com/UZGG86035

But I'm not sure if I'm not reinventing the wheel. Are there any easier, commonly known approach to do that?


Solution

  • In C++11, I'd advise a token-based approach.

    You register an observer. An observer is just a std::function<void(Signature...)>.

    The registration function return a token, a std::shared_ptr<void>. So long as that returned shared_ptr is valid, the broadcaster will continue to broadcast to that listener.

    The listener is now responsible for maintaining that std::shared_ptr lifetime.

    Inside the broadcaster, you hold a weak_ptr and .lock() it before broadcasting. If I don't really need to unregister (usually I do not), I lazily clean up my list of weak_ptrs. Otherwise, I the shared_ptr I return has a deletion function that does the unregistration.


    Alternatively, your listeners are shared_ptr<std::function<void(Args...)>>, and internally you store weak_ptr to same.

    In this model, you cannot inject an unregistraiton function easily. However, it does mean they can use the aliasing constructor themselves to tightly bind the lifetime of the callback to themselves, assuming they are managed by a shared_ptr.

    In my experience, simply having listeners maintain a std::vector<token> is sufficient. If they have a more complex listening relationship they can do more work, maintaining keys and the like.

    Hybrid models are also possible.


    Both of these are acceptable for not-thread-safe broadcasting, and can be written in a few dozen lines of code.

    Thread-safe broadcasting gets tricky. Often I find you are better off using a message-passing pattern for this rather than the alternatives, as this reduces the difficulty in reasoning about concurrency slightly.

    This also doesn't deal with situations where you want to register listeners willy-nilly, and broadcaster and listener lifetime is like popcorn.