Reputation: 25
I have been playing around with the new C++11 standard lately and decided to create a basic event-handling system. The code below provides a small example of my current implementation.
#include <functional>
#include <vector>
#include <iostream>
template <typename Event>
class EventBroadcaster
{
public:
typedef std::function<void(const Event&)> Connection;
void connect(Connection&& connection)
{
connections.push_back(std::move(connection));
}
void signal(const Event& event)
{
for (const auto& connection : connections)
{
connection(event);
}
}
private:
std::vector<Connection> connections;
};
struct MouseMotion
{
int x = 0;
int y = 0;
};
class Input : public EventBroadcaster<MouseMotion>
{
public:
void process()
{
MouseMotion mouseMotion;
mouseMotion.x = 10;
mouseMotion.y = 20;
signal(mouseMotion);
}
};
int main()
{
int x = 0;
int y = 0;
Input input;
input.connect([&](const MouseMotion& e){
x += e.x;
y += e.y;
});
input.process();
std::cout << x << "," << y << std::endl; // Output: 10,20
return 0;
}
The above solution does work quite nicely if the Input
class would only broadcast a single event. There might however be the case that the Input
class would want to be able to send KeyPress
events besides just MouseMotion
events.
I thought about using multiple inheritance. Making Input
inherit both EventBroadcaster<MouseMotion>
and EventBroadcaster<KeyPress>
. This results in compiler errors warning of ambiguous functions. The solution provided in the following answer Multiple Inheritance Template Class does work for the protected signal
function, but not for the public connect
function called outside the Input
class.
Besides multiple inheritance I wondered if variadic-templates could help my with my problem. I have looked at (partial) template specialization and unpacking variadic-templates. But have been unable to come with an (elegant) solution.
What would be the best way to support multiple event types?
Upvotes: 1
Views: 3782
Reputation: 275600
I'd make EventBroadcaster<T>
an implementation detail, and instead expose EventBroadcasters<Ts...>
.
EventBroadcasters<Ts...>
owns EventBroadcaster<Ts>...
has template methods connect<U>
and signal<U>
, and forwards these to the EventBroadcaster<U>
. This should fail to compile if U
is not in Ts...
.
So now you signal(mouseMotion)
, which resolves to signal<MouseMotion>(mouseMotion)
, which connects through to the correct broadcaster.
When you connect
to listen, similarly so long as you are using the right kind of std::function
things work. And if not, you can just pass in the type of the Event
you are listening for.
There are ways to make it even more magical, and enable full-on proper overload resolution that you'd want, but that gets really tricky. Ie, you instrument up connect
to do SFINAE and manually dispatch the passed in lambda (by examining its operator()
) to the correct std::function
override. However, simply being able to say connect<MouseMotion>([&](MouseMotion m){...})
should be an improvement.
(SFINAE technique would consist of checking which std::function
type out of a list can be constructed from your passed in U
, and if there is one unique such, using that one. Also, check if U
is such a std::function
(or a cv variant of such). It isn't ridiculously hard, but it is very messy looking, and in my experience most people don't like the type-spew and prefer to just specify <MouseMotion>
.)
UPDATE:
The following code block provides an implementation of this answer.
#include <functional>
#include <vector>
#include <iostream>
namespace detail
{
template <typename Event>
class EventBroadcaster
{
public:
typedef std::function<void(const Event&)> Connection;
void connect(Connection&& connection)
{
connections.push_back(std::move(connection));
}
void signal(const Event& event)
{
for (const auto& connection : connections)
{
connection(event);
}
}
private:
std::vector<Connection> connections;
};
template <typename T> struct traits
: public traits<decltype(&T::operator())> {};
template <typename C, typename R, typename A>
struct traits<R(C::*)(const A&) const>
{
typedef A type;
};
}
template <typename... Events>
class EventBroadcaster
: detail::EventBroadcaster<Events>...
{
public:
template <typename Connection>
void connect(Connection&& connection)
{
typedef typename detail::traits<Connection>::type Event;
detail::EventBroadcaster<Event>& impl = *this;
impl.connect(std::move(connection));
}
template <typename Event>
void signal(const Event& event)
{
detail::EventBroadcaster<Event>& impl = *this;
impl.signal(event);
}
virtual void processEvents() = 0;
};
struct MouseMotion
{
int x = 0;
int y = 0;
};
struct KeyPress
{
char c;
};
class Input
: public EventBroadcaster<MouseMotion, KeyPress>
{
public:
void processEvents()
{
MouseMotion mouseMotion;
mouseMotion.x = 10;
mouseMotion.y = 20;
signal(mouseMotion);
KeyPress keyPress;
keyPress.c = 'a';
signal(keyPress);
}
};
int main()
{
int x = 0;
int y = 0;
char c = '~';
Input input;
input.connect([&](const MouseMotion& e){
x += e.x;
y += e.y;
});
input.connect([&](const KeyPress& e){
c = e.c;
});
input.processEvents();
std::cout << c << " " << x << "," << y << std::endl; // Output: a 10,20
return 0;
}
Upvotes: 4
Reputation: 126462
If that is OK to you, you can solve this problem by adding the using
directives for KeyPress
in your Input
class and store your lambda in a std::function<>
object rather than using auto
:
class Input :
public EventBroadcaster<MouseMotion>,
public EventBroadcaster<KeyPress>
{
public:
using EventBroadcaster<MouseMotion>::signal;
using EventBroadcaster<MouseMotion>::connect;
// ADD THESE!
using EventBroadcaster<KeyPress>::signal;
using EventBroadcaster<KeyPress>::connect;
Input() {}
virtual ~Input() {}
void process()
{
MouseMotion mouseMotion;
mouseMotion.x = 10;
mouseMotion.y = 20;
signal(mouseMotion);
}
};
int main()
{
Input input;
int myX = 0;
int myY = 0;
// STORE THE LAMBDA IN A function<> OBJECT:
std::function<void(const MouseMotion&)> onMouseMotion = [&](const MouseMotion& e){
myX += e.x;
myY += e.y;
};
// THIS WILL NOT BE AMBIGUOUS:
input.connect(onMouseMotion);
input.process();
std::cout << myX << "," << myY << std::endl; // Output: 10,20
return 0;
}
UPDATE:
What follows is an improvement of the above solution that does not require wrapping the lambda handler in an std::function
object. It is enough to change the definition of the connect
method as follows, and let SFINAE take care of ruling out the inappropriate overloads:
template<typename F>
void connect(F connection, decltype(std::declval<F>()(std::declval<Event>()))* = nullptr)
{
connections.push_back(connection);
}
It is now possible to call connect()
this way:
input.connect([&](const MouseMotion& e){
myX += e.x;
myY += e.y;
});
input.connect([&](const KeyPress& e){
std::cout << e.c;
});
Upvotes: 1