4
\$\begingroup\$

This is my first shot at implementing a fully static version of the Observer pattern in C++11. It is static in that all notifications are dispatched via CRTP, and therefore there is no virtual overhead. It also has the benefit of catching unimplemented notifications at compile time, by checking available overloads based on the Subject type. Its primary restriction is that all relevant observers must be known at compile time.

The motivation is to use this on an embedded system to help decouple systems while additionally providing better type safety. It appears that the initial output of this is exactly how I want it (zero cost) and the restriction of needing to know all observers in advance is reasonable given that the systems are well defined, and typically not dynamic. The platforms I work with typically do not allow malloc/new due to their ability to fail.

// static_observer header file
#include <utility>
#include <tuple>

enum class EventType;

// CRTP Abstract Base class for implementing static subject.
// Example Subclass Usage -- Printing Observer:
//   class Printer : public Observer<Printer> {
//   public:
//     Printer() : timesTriggered_(0) {}
//     template <typename... Args>
//     void OnNotify(Pressure<Args...> &subject, EventType event) {
//       std::cout << "Observer ID: " << this->GetID() << std::endl;
//       switch (event) {
//       case EventType::UNKNOWN: {
//         std::cout << "Unknown Event -- Event #" << timesTriggered_++
//                   << std::endl;
//         std::cout << "Pressure: " << subject.GetPressure() << std::endl;
//         break;
//       }
//       default: { break; }
//       }
//     }
//   
//   private:
//     int timesTriggered_;
//   };
// Note: All Observers must implement OnNotify for any subject types they wish to observe
//       Any unimplemented subject types that are used will result in a compiler error
template <typename Base> class Observer {
public:
  Observer() : obsID_(obsIDTracker_++) {}
  template <typename T> void OnNotifyImpl(T &subject, EventType event) {
    static_cast<Base *>(this)->OnNotify(subject, event);
  }
  int GetID() const { return obsID_; }
private:
  int obsID_;
  static int obsIDTracker_;
};
template <typename base> int Observer<base>::obsIDTracker_ = 0;

// Recursive helper structs for implementing calls to all observers held within subjects
template <int N, typename T, typename... Args> struct NotifyHelper {
  static void NotifyImpl(T &subject, EventType event,
                         std::tuple<Args...> &obs) {
    std::get<sizeof...(Args) - N>(obs).OnNotifyImpl(subject, event);
    NotifyHelper<N - 1, T, Args...>::NotifyImpl(subject, event, obs);
  }
};
template <typename T, typename... Args> struct NotifyHelper<0, T, Args...> {
  static void NotifyImpl(T &subject, EventType event,
                         std::tuple<Args...> &obs) {}
};

// CRTP Abstract Base class for implementing static subject.  
// Example Subclass Usage -- Pressure Sensor:
//   template <typename... Obs>
//   class Pressure : public Subject<Pressure<Obs...>, Obs...> {
//   public:
//     typedef Subject<Pressure<Obs...>, Obs...> BaseType;
//     Pressure(std::tuple<Obs &...> &&observers, int pressure)
//         : BaseType(std::move(observers)), pressure_(pressure) {}
//     void Change(int value) {
//       pressure_ = value;
//       this->NotifyAll(EventType::UNKNOWN);
//     }
//     int GetPressure() const { return pressure_; }
//   
//   private:
//     int pressure_;
//   };
// See MakeSubject function for instance usage
template <typename T, typename... Obs> class Subject {
public:
  static const int NumberOfObservers = sizeof...(Obs);
  Subject(std::tuple<Obs &...> &&obs) : observers(obs) {}
  void NotifyAll(EventType event) {
    NotifyHelper<NumberOfObservers, T, Obs &...>::NotifyImpl(
        *static_cast<T *>(this), event, observers);
  }

private:
  std::tuple<Obs &...> observers;
};

// Binding function for use with MakeSubject
//   Arguments: observer objects to observe subject notifications
//   Return:    tuple of references to observers
template <typename... Obs> std::tuple<Obs &...> BindObservers(Obs &... obs) {
  return std::tuple<Obs &...>(obs...);
}
// Creator to ease subject creation
//   Template Arguments: Subject subclass type
//   Arguments: Result from BindObservers
//              Any constructor arguments for Subject subclass
//   Return:    Subject subclass
// Example Usage:
// auto pressure = MakeSubject<Pressure>(BindObservers(printerObs), initialPressure);
template <template <typename...> class T, typename... Args, typename... Obs>
T<Obs...> MakeSubject(std::tuple<Obs &...> &&obs, Args &&... args) {
  return T<Obs...>(std::move(obs), args...);
}

Example using the subclasses in the commented examples:

int main() {
  Printer printerObs1;
  Printer printerObs2;
  const int initialPressure = 1;

  auto pressure = MakeSubject<Pressure>(
      BindObservers(printerObs1, printerObs2), initialPressure);
  pressure.Change(12);
}

Output:

Observer ID: 0
Unknown Event -- Event #0
Pressure: 12
Observer ID: 1
Unknown Event -- Event #0
Pressure: 12    

In addition to general critique, I also have a couple specific concerns/questions:

  • While the observers will typically be statically stored, there is nothing preventing a programmer from creating an observer in automatic storage then storing a reference to it in a subject that is retained past its destruction. I don't know if there's an easy way to prevent this, but it's a potential issue.

  • In the Subject super class, I needed to do a cast: *static_cast<T *>(this) as opposed to casting *this to type T (where T is the subclass type.) This allows the Observer to overload based on T, but I'm worried it might not be safe in some scenarios. Is this valid?

  • I dislike the verbosity required for creating a Subject subclass. Needing to type out the parameter pack so many times seems like it could be hard to work with, but with a good example maybe its okay?

  • The MakeSubject function seems a little verbose too. Same as above -- is it an okay style if it's simply well-documented?

\$\endgroup\$
3
\$\begingroup\$

Personally (so you can take or leave this advice as this is just a personal pref) I find your style over compact and thus harder than necessary to read:

template <typename Base> class Observer {

// I split this across two lines:
template <typename Base>
class Observer {

Also:

class Observer {
public:
  Observer() : obsID_(obsIDTracker_++) {}
  template <typename T> void OnNotifyImpl(T &subject, EventType event) {
    static_cast<Base *>(this)->OnNotify(subject, event);
  }
  int GetID() const { return obsID_; }
private:

// Everything is so squashed to the left:
// Bit more space to help readability never heart (and the compiler don't care).

template <typename Base>
class Observer
{
    public:
        Observer()
            : obsID_(obsIDTracker_++)
        {}

        template <typename T>
        void OnNotifyImpl(T &subject, EventType event)
        {
            static_cast<Base *>(this)->OnNotify(subject, event);
        }

        // I would still do this all in one line.
        int GetID() const { return obsID_; }

    private:
        int obsID_;
        static int obsIDTracker_;
};

This line confused me for a while:

Subject(std::tuple<Obs &...> &&obs)                : observers(obs) {}
                        //   ^^  rvalue reference              ^^^ no move

You are giving the option to pass by r-value reference this implying you are going to rip the guts out of the object. But you are actually just making a copy into observers. A cheap copy as the tuple only contains references. But I think the interface is better if you pass by const reference to more accurately reflect the usage semantics better.

Subject(std::tuple<Obs &...> const& obs)                : observers(obs) {}

OK found why you have r-value references:

template <template <typename...> class T, typename... Args, typename... Obs>
T<Obs...> MakeSubject(std::tuple<Obs &...> &&obs, Args &&... args) {
  return T<Obs...>(std::move(obs), args...);
}

For the Obs is is unnecessary; as they are held in the tuple by reference. So std::move() here does not buy you much (apart from me scratching my head for 10 minutes :-)

On the other hand Args it does seem correct to pass by r-value reference, but because you don't use std::move they are decaying into references when you pass them to the constructor (I could be wrong on this last point (still getting my head around this new fangled stuff).

While the observers will typically be statically stored, there is nothing preventing a programmer from creating an observer in automatic storage then storing a reference to it in a subject that is retained past its destruction. I don't know if there's an easy way to prevent this, but it's a potential issue.

If there is no new usage I can not contrive of a situation where this would happen. As the Oberver must be constructed before the Subject because you add add Observers during the Subject construction.

Also I would argue that typical usage should be everything automatic not static.

In the Subject super class, I needed to do a cast: *static_cast(this) as opposed to casting *this to type T (where T is the subclass type.) This allows the Observer to overload based on T, but I'm worried it might not be safe in some scenarios. Is this valid?

This looks fine.

I dislike the verbosity required for creating a Subject subclass. Needing to type out the parameter pack so many times seems like it could be hard to work with, but with a good example maybe its okay?

You have done the hard work. So that the users of your lib don't need to worry about it. So I think you have hidden the details rather well. Its still verbose but with some good examples and an explanation you will be fine.

The MakeSubject function seems a little verbose too. Same as above -- is it an okay style if it's simply well-documented?

Same comment as above.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thank you very much for the comments! I appreciate them greatly. I agree on the whitespace, and will adopt some of your changes (need to stick to LLVM-style for policy reasons.) I'm still trying to wrap my head around best practices for r-value references, so I greatly appreciate your help there. Is there a simple way to pass the Args pack by std::move? Thanks again Loki! I was starting to worry no one would reply. (Accepted) \$\endgroup\$ – Sam Cristall Jan 12 '14 at 23:29
  • \$\begingroup\$ Nevermind -- figured out the Args pack: std::move<Args>(args)... \$\endgroup\$ – Sam Cristall Jan 12 '14 at 23:39

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.