是否可以在C++中执行此lambda事件管理器?

dan*_*jar 6 c++ collections lambda templates initializer-list

我想编写一个支持传递任意数量参数的事件管理器.为了向您展示表单,这是一个示例.请注意,一个目标是不需要为每个事件定义类.相反,事件由字符串名称表示.首先,让4个侦听器注册到同一个事件.它们接受的参数数量不同.

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;
});
Run Code Online (Sandbox Code Playgroud)

现在用一些参数激活事件.events.fire("key", {42, "John"});这应该调用匹配部分或全部参数的已注册lambda.例如,此调用应为我们注册的五个侦听器生成以下结果.

  1. 打印"按下一个键".
  2. 打印"按下代码42的按键".
  3. 打印"John使用代码42按下键".
  4. 抛出异常,因为侦听器与签名不匹配.
  5. 抛出异常,因为侦听器与签名不匹配.

是否有可能在C++中实现此行为?如果是这样,我怎样才能将不同的回调存储在一个集合中,同时仍然能够将它们强制转换为使用不同数量的参数进行调用?我认为这个任务并不容易,所以每个提示都有帮助.

And*_*owl 4

我同意 Luc 的观点,即类型安全的方法可能更合适,但以下解决方案或多或少可以满足您的需求,但有一些限制:

  1. 参数类型必须是可复制的;
  2. 参数总是被复制,从不移动;
  3. 当且仅当前 N 个参数的类型与处理程序参数的类型完全fire()匹配时,才会调用具有 N 个参数的处理程序,并且不执行隐式转换(例如,从字符串文字到);std::string
  4. 处理程序不能是具有多个重载的函子operator ()

这就是我的解决方案最终允许您编写的内容:

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);
}
Run Code Online (Sandbox Code Playgroud)

第一次调用fire()将产生以下输出:

Got a 42
Got a 42 and a hi
Bad arity!
Bad argument!
Bad arity!
Run Code Online (Sandbox Code Playgroud)

第二次调用将产生以下输出:

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!
Run Code Online (Sandbox Code Playgroud)

这是一个活生生的例子

实现基于boost::any. 它的核心是dispatcher函子。它的调用运算符采用类型擦除参数的向量,并将它们分派给构造它的可调用对象(您的处理程序)。如果参数类型不匹配,或者处理程序接受的参数多于提供的参数,它只会将错误打印到标准输出,但如果您愿意或执行任何您喜欢的操作,您可以让它抛出:

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;
};
Run Code Online (Sandbox Code Playgroud)

然后有几个实用程序用于从处理程序函子创建调度程序(有一个类似的实用程序用于从函数指针创建调度程序):

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)); 
}
Run Code Online (Sandbox Code Playgroud)

帮助function_traits器是一个简单的特征,用于确定处理程序的类型,因此我们可以将它们作为模板参数传递给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...>;
};
Run Code Online (Sandbox Code Playgroud)

显然,如果您的处理程序是一个具有多个重载调用运算符的函子,那么整个事情将无法工作,但希望这个限制对您来说不会太严重。

最后,该类event_dispatcher允许您通过调用将类型擦除的处理程序存储在多重映射中,并在您使用适当的键和适当的参数listen()调用时调用它们(您的对象将是此类的实例):fire()events

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;
};
Run Code Online (Sandbox Code Playgroud)

再一次,完整的代码可以在这里找到。