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?
1 Answer 1
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>
forstd::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 beclicked
: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 alistener
.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 ofstd::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" :/
-
\$\begingroup\$ Awesome feedback, every point is useful to me - thanks :-) FYI I'll update the question with the improved code \$\endgroup\$Lars Melchior– Lars Melchior2015年07月30日 10:10:35 +00:00Commented Jul 30, 2015 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– Morwenn2015年07月30日 10:13:42 +00:00Commented Jul 30, 2015 at 10:13
event
intosignal
,notify
intoemit
andobserve
intoconnect
, it sems that what you've got are indeed signals :p \$\endgroup\$observe
since it's the listener/observes which should "observe", not the event. Or at least I think so. \$\endgroup\$