Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 that one goal is to not need a class definition for every event. Instead, events are represented by string names. First, lets register four listeners to the same event. They differ in the number of parameters they accept.

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 fire the event with some arguments. events.fire("key", {42, "John"}); This should call registered lambdas that match some or all of the arguments. For example, this call should produce the following result for the five listeners we registered.

  1. Print "Pressed a key."
  2. Print "Pressed key with code 42."
  3. Print "John pressed key with code 42."
  4. Throw exception because listener doesn't match signature.
  5. Throw exception because listener doesn't match signature.

Is it possible to achieve this behavior in C++? If so, how can I store the different callbacks in a collection while still being able to cast them back for calling with different numbers of parameters? I think this task is not easy so every hint helps.

like image 950
danijar Avatar asked Sep 07 '14 20:09

danijar


People also ask

Does C support Lambda function?

Significance of Lambda Function in C/C++ Lambda Function − Lambda are functions is an inline function that doesn't require any implementation outside the scope of the main program. Lambda Functions can also be used as a value by the variable to store.

What are the two conditions required for using a Lambda function in a stream?

In order to match a lambda to a single method interface, also called a "functional interface", several conditions need to be met: The functional interface has to have exactly one unimplemented method, and that method (naturally) has to be abstract.


2 Answers

I agree with Luc's point that a type-safe approach is probably more appropriate, but the following solution does more or less what you want, with a few limitations:

  1. Argument types must be copyable;
  2. Arguments are always copied, never moved;
  3. A handler with N parameters is invoked if and only if the types of the first N arguments to fire() match exactly the types of the handler's parameters, with no implicit conversions being performed (e.g. from string literal to std::string);
  4. Handlers cannot be functors with more than one overloaded operator ().

This is what my solution eventually 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 produce the following output:

Got a 42
Got a 42 and a hi
Bad arity!
Bad argument!
Bad arity!

While the second call will produce 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 live example.

The implementation is based on boost::any. The heart of it is the dispatcher functor. Its call operator takes a vector of type-erased arguments and dispatches them to the callable object with which it is constructed (your handler). If the arguments type don't match, or if the handler accepts more arguments than provided, it just prints an error to the standard output, but you can make it throw if you wish or do whatever 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 a couple of utilities for creating dispatchers out of a handler functor (there is a similar utility for creating dispatchers out of 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 trait to figure out the types of the handler so we can pass them as template arguments to 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...>;
};

Clearly this whole thing won't work if your handler is a functor with several overloaded call operators, but hopefully this limitation won't be too severe for you.

Finally, the event_dispatcher class allows you storing type-erased handlers in a multimap by calling listen(), and invokes them when you call fire() with the appropriate key and the appropriate 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;
};

Once again, the whole code is available here.

like image 158
Andy Prowl Avatar answered Oct 19 '22 08:10

Andy Prowl


one goal is to not need a class definition for every event.

That’s a good sign that you want something else than C++ for your purposes, since it has no dynamic reflection capabilities. (If you do use something more dynamic but still need to interface with C++, you would need to bridge the gap though, so this answer may or may not still be useful for that.)

Now while it is possible to build a (limited) dynamic system, you should ask yourself if it is what you really want to do. E.g. if you ‘close the world’ of events and their callback signatures, you would retain a lot of type-safety:

// assumes variant type, e.g. 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, here’s 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)>;

/*
 * E.g.:
 * callback_type f = erase<int>([captures](int code) { ... });
 */

Coliru demo.

If you have a type trait that can guess the signature of a callable type, you can write an erase that uses it (while still allowing the user to fill it in for those cases where it can’t be deduced). I’m not using one in the example because that’s another can of worms.

†: ‘any‘ meaning any callable object accepting some numbers of copyable arguments, returning void—you can relax the requirements on arguments by using a move-only wrapper similar to boost::any

like image 22
Luc Danton Avatar answered Oct 19 '22 08:10

Luc Danton