Is it possible to do this lambda event manager in c ++?

I want to write an event manager that supports passing an arbitrary number of arguments. To show you the form, here is an example. Please note: one goal is not to require a class definition for each event. Instead, events are represented by line names. First, let's register four listeners in the same event. They differ in the number of accepted parameters.

Events events; events.listen("key", [=] { cout << "Pressed a key." << endl; }); events.listen("key", [=](int code) { cout << "Pressed key with code " << code << "." << endl; }); events.listen("key", [=](int code, string user) { cout << user << " pressed key with code " << code << "." << endl; }); events.listen("key", [=](int code, string user, float duration) { cout << user << " pressed key with code " << code << " for " << duration << " seconds." << endl; }); events.listen("key", [=](string user) { cout << user << " pressed a key." << endl; }); 

Now turn on the event with some arguments. events.fire("key", {42, "John"}); This should cause registered lambdas that match some or all of the arguments. For example, this call should give the following result for the five listeners that we registered.

  • Print "Press the key." "
  • Print "Pressed key with code 42".
  • Seal "John pressed key code 42."
  • Throw exception, because the listener does not match the signature.
  • Throw exception, because the listener does not match the signature.

Is it possible to achieve this behavior in C ++? If so, how can I store various callbacks in a collection, while still having the ability to drop them for a call with a different number of parameters? I think this task is not easy, so every hint helps.

+6
source share
2 answers

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 .

+4
source

One goal is not to require a class definition for each event.

This is a good sign that you want something other than C ++ for your purposes, because it does not have dynamic reflection capabilities. (If you are using something more dynamic, but still need to interact with C ++, you will need to eliminate the gap, so this answer may or may not be useful for this.)

Now that you can build a (limited) dynamic system, you should ask yourself if this is really what you really want to do. For instance. if you close the world of events and their callback signatures, you would maintain greater type safety:

 // assumes variant type, eg Boost.Variant using key_callback = variant< function<void(int)> // code , function<void(int, string)> // code, user , function<void(int, string, float)> // code, user, duration , function<void(string)> // user >; using callback_type = variant<key_callback, …more event callbacks…>; 

In the spirit of sticking to your requirement, though, how to store any callback, and still be able to call it:

 using any = boost::any; using arg_type = std::vector<any>; struct bad_signature: std::exception {}; struct bad_arity: bad_signature {}; struct bad_argument: bad_signature { explicit bad_argument(int which): which{which} {} int which; }; template<typename Callable, typename Indices, typename... Args> struct erased_callback; template<typename Callable, std::size_t... Indices, typename... Args> struct erased_callback<Callable, std::index_sequence<Indices...>, Args...> { // you can provide more overloads for cv/ref quals void operator()(arg_type args) { // you can choose to be lax by using < if(args.size() != sizeof...(Args)) { throw bad_arity {}; } callable(restore<Args>(args[Indices], Indices)...); } Callable callable; private: template<typename Arg> static Arg&& restore(any& arg, int index) { using stored_type = std::decay_t<Arg>; if(auto p = boost::any_cast<stored_type>(&arg)) { return std::forward<Arg>(*p); } else { throw bad_argument { index }; } } }; template< typename... Args, typename Callable , typename I = std::make_index_sequence<sizeof...(Args)> > erased_callback<std::decay_t<Callable>, I, Args...> erase(Callback&& callback) { return { std::forward<Callback>(callback) }; } // in turn we can erase an erased_callback: using callback_type = std::function<void(arg_type)>; /* * Eg: * callback_type f = erase<int>([captures](int code) { ... }); */ 

Demo version of Coliru .

If you have a type trait that can guess the signature of the called type, you can write an erase that uses it (at the same time allowing the user to fill it out in cases where it cannot be inferred). I do not use one in the example, because it could be another worm.

†: "any", meaning any object being called, taking some numbers of copied arguments, returning void - you can relax the argument requirements by using a wrapper just for moving, similar to boost::any

+3
source

Source: https://habr.com/ru/post/974944/


All Articles