6
\$\begingroup\$

While writing a C++ GUI application more or less from scratch I needed some form of an event-listener system, preferably using lambdas. An event should be able to have multiple listeners and the user should not have to worry about the lifetime of the objects. I came up with this bit using shared and weak pointers to determine object lifetime:

#include <functional>
#include <list>


template <typename ... Args> struct event:public std::shared_ptr<std::list<std::function<void(Args...)>>>{

  using handler = std::function<void(Args...)>;
  using listener_list = std::list<handler>;

  struct listener{
    std::weak_ptr<listener_list> the_event;
    typename listener_list::iterator it;

    listener(){ }

    listener(event & s,handler f){
      observe(s,f);
    }

    listener(listener &&other){
      the_event = other.the_event;
      it = other.it;
      other.the_event.reset();
    }

    listener(const listener &other) = delete;

    listener & operator=(const listener &other) = delete;

    listener & operator=(listener &&other){
      reset();
      the_event = other.the_event;
      it = other.it;
      other.the_event.reset();
      return *this;
    }

    void observe(event & s,handler f){
      reset();
      the_event = s;
      it = s->insert(s->end(),f);
    }

    void reset(){
      if(!the_event.expired()) the_event.lock()->erase(it);
      the_event.reset();
    }

    ~listener(){ reset(); }
  };

  event():std::shared_ptr<listener_list>(std::make_shared<listener_list>()){ }
  event(const event &) = delete;
  event & operator=(const event &) = delete;

  void notify(Args... args){
    for(auto &f:**this) f(args...);
  }

  listener connect(handler h){
    return listener(*this,h);
  }

};

Example usage:

#include <iostream>

using click_event = event<float,float>;

struct gui_element{
  click_event click;
  void mouse_down(float x,float y){ click.notify(x, y); }
};

int main(int argc, char **argv) {
  gui_element A,B;
  click_event::listener listener_1,listener_2;

  listener_1.observe(A.click,[](float x,float y){ std::cout << "l1 : A was clicked at " << x << ", " << y << std::endl; });
  listener_2.observe(B.click,[](float x,float y){ std::cout << "l2 : B was clicked at " << x << ", " << y << std::endl; });

  {
  auto temporary_listener = A.click.connect([](float x,float y){ std::cout << "tmp: A was clicked at " << x << ", " << y << std::endl; });
  // A has two listeners, B has one listeners
  A.mouse_down(1, 0);
  B.mouse_down(0, 1);
  }

  listener_2 = std::move(listener_1);

  // A has one listener, B has no listeners
  A.mouse_down(2, 0);
  B.mouse_down(0, 2);
}

Output:

l1 : A was clicked at 1, 0
tmp: A was clicked at 1, 0
l2 : B was clicked at 0, 1
l1 : A was clicked at 2, 0

While the usage is exactly how I wanted, I am not sure the implementation is elegant or optimal. Any way how to improve this?

\$\endgroup\$
  • \$\begingroup\$ In other words, you reimplemented signals, right? :) \$\endgroup\$ – Morwenn Jul 30 '15 at 9:08
  • \$\begingroup\$ I'm not sure about the correct terminology but something like that, yeah ;-) \$\endgroup\$ – Lars Jul 30 '15 at 9:13
  • \$\begingroup\$ I you rename event into signal, notify into emit and observe into connect, it sems that what you've got are indeed signals :p \$\endgroup\$ – Morwenn Jul 30 '15 at 9:14
  • \$\begingroup\$ So are the terms event and listener incorrect then? (this is my first real gui project so I chose the names to sound intuitive to me) \$\endgroup\$ – Lars Jul 30 '15 at 9:17
  • \$\begingroup\$ No no, they are fine. The only strange name is observe since it's the listener/observes which should "observe", not the event. Or at least I think so. \$\endgroup\$ – Morwenn Jul 30 '15 at 9:21
4
\$\begingroup\$

As I said in the comments, your system is pretty close to a system of signals and slots. Congratulations if you never heard of the pattern before, that's an excellent way to implement an the observer design pattern! I still have few notes:

  • I don't know which compiler you use, but mine won't compile your code unless I include <memory> for std::shared_ptr. I suppose that it may be transitively included from <functional> or <list> in your implementation. Never rely on transitive includes from the standard library and always include the exact header that contains what you need.

  • Qt, which is the emblematic library when it comes to signals and slots, tends to use past participle for its signas names. For example, instead of click, it would be clicked:

    button.clicked.connect(/* whatever */);
    

    It allows to read the line easily as "when button is clicked, do whatever". Also, it makes the object look more like a trigger and less like an action.

  • From a design point of view, connect being a method taking only one function, I would expect it to add the function to the list, not to return a listener.

  • You should encapsulate std::shared_ptr<std::list<std::function<void(Args...)>>> instead of inheriting from it. Frankly, you don't want people to expose every method of std::shared_ptr. You don't want people to think of pointers when they want events, right?

The truth is my answer isn't really personal. Since your class obviously looks like a signal class, everything I am saying tends to mean "make your design closer to those of existing signal classes like Boost.Signals2" :/

\$\endgroup\$
  • \$\begingroup\$ Awesome feedback, every point is useful to me - thanks :-) FYI I'll update the question with the improved code \$\endgroup\$ – Lars Jul 30 '15 at 10:10
  • 2
    \$\begingroup\$ @Lars Don't. Once a question is answered, you can't edit the code in the question in the manner that would invalidate the answers. That's one of the main site rules. See our help centre for more information. \$\endgroup\$ – Morwenn Jul 30 '15 at 10:13

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.