I agree with Luke that a type approach is safe, probably more appropriate, but the following solution does more or less what you want, with a few limitations:
- Argument types must be copied;
- Arguments are always copied, never moved;
- A handler with N parameters is called if and only if the types of the first N arguments in
fire() exactly match the types of handler parameters, without performing implicit conversions (for example, from a string literal to std::string ); - Handlers cannot be functors with multiple
operator () overloads.
This is what my solution ultimately allows you to write:
void my_handler(int x, const char* c, double d) { std::cout << "Got a " << x << " and a " << c << " as well as a " << d << std::endl; } int main() { event_dispatcher events; events.listen("key", [] (int x) { std::cout << "Got a " << x << std::endl; }); events.listen("key", [] (int x, std::string const& s) { std::cout << "Got a " << x << " and a " << s << std::endl; }); events.listen("key", [] (int x, std::string const& s, double d) { std::cout << "Got a " << x << " and a " << s << " as well as a " << d << std::endl; }); events.listen("key", [] (int x, double d) { std::cout << "Got a " << x << " and a " << d << std::endl; }); events.listen("key", my_handler); events.fire("key", 42, std::string{"hi"}); events.fire("key", 42, std::string{"hi"}, 3.14); }
The first call to fire() will lead to the following output:
Got a 42 Got a 42 and a hi Bad arity! Bad argument! Bad arity!
The second call will output the following output:
Got a 42 Got a 42 and a hi Got a 42 and a hi as well as a 3.14 Bad argument! Bad argument!
Here is a living example .
The implementation is based on boost::any . The heart of this is the dispatcher functor. Its call statement takes a vector of arguments of type erase and sends them to the called object with which it is built (your handler). If the type of the arguments does not match, or if the handler takes more arguments than it provided, it simply prints an error for standard output, but you can make it throw if you want or do what you prefer:
template<typename... Args> struct dispatcher { template<typename F> dispatcher(F f) : _f(std::move(f)) { } void operator () (std::vector<boost::any> const& v) { if (v.size() < sizeof...(Args)) { std::cout << "Bad arity!" << std::endl; // Throw if you prefer return; } do_call(v, std::make_integer_sequence<int, sizeof...(Args)>()); } private: template<int... Is> void do_call(std::vector<boost::any> const& v, std::integer_sequence<int, Is...>) { try { return _f((get_ith<Args>(v, Is))...); } catch (boost::bad_any_cast const&) { std::cout << "Bad argument!" << std::endl; // Throw if you prefer } } template<typename T> T get_ith(std::vector<boost::any> const& v, int i) { return boost::any_cast<T>(v[i]); } private: std::function<void(Args...)> _f; };
Then there are several utilities for creating dispatchers from a handler functor (there is a similar utility for creating dispatchers from function pointers):
template<typename T> struct dispatcher_maker; template<typename... Args> struct dispatcher_maker<std::tuple<Args...>> { template<typename F> dispatcher_type make(F&& f) { return dispatcher<Args...>{std::forward<F>(f)}; } }; template<typename F> std::function<void(std::vector<boost::any> const&)> make_dispatcher(F&& f) { using f_type = decltype(&F::operator()); using args_type = typename function_traits<f_type>::args_type; return dispatcher_maker<args_type>{}.make(std::forward<F>(f)); }
The function_traits helper is a simple feature for defining handler types, so we can pass them as template arguments to the dispatcher :
template<typename T> struct function_traits; template<typename R, typename C, typename... Args> struct function_traits<R(C::*)(Args...)> { using args_type = std::tuple<Args...>; }; template<typename R, typename C, typename... Args> struct function_traits<R(C::*)(Args...) const> { using args_type = std::tuple<Args...>; };
Obviously, all this will not work if your handler is a functor with several overloaded call operators, but I hope this restriction will not be too heavy for you.
Finally, the event_dispatcher class allows event_dispatcher to store event_dispatcher types handlers in a multimar by calling listen() and calling them when fire() called with the corresponding key and corresponding arguments (your events object will be an instance of this class):
struct event_dispatcher { public: template<typename F> void listen(std::string const& event, F&& f) { _callbacks.emplace(event, make_dispatcher(std::forward<F>(f))); } template<typename... Args> void fire(std::string const& event, Args const&... args) { auto rng = _callbacks.equal_range(event); for (auto it = rng.first; it != rng.second; ++it) { call(it->second, args...); } } private: template<typename F, typename... Args> void call(F const& f, Args const&... args) { std::vector<boost::any> v{args...}; f(v); } private: std::multimap<std::string, dispatcher_type> _callbacks; };
And again, all the code is available here .