C++ 是否可以在C++;?
我想编写一个支持传递任意数量参数的事件管理器。为了向您展示表单,下面是一个示例。请注意,一个目标是不需要为每个事件定义类。相反,事件由字符串名称表示。首先,让我们为同一事件注册四个侦听器。它们接受的参数数量不同C++ 是否可以在C++;?,c++,templates,collections,lambda,initializer-list,C++,Templates,Collections,Lambda,Initializer List,我想编写一个支持传递任意数量参数的事件管理器。为了向您展示表单,下面是一个示例。请注意,一个目标是不需要为每个事件定义类。相反,事件由字符串名称表示。首先,让我们为同一事件注册四个侦听器。它们接受的参数数量不同 Events events; events.listen("key", [=] { cout << "Pressed a key." << endl; }); events.listen("key", [=](int code) { cout
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;
});
事件;
事件。侦听(“键”,[=]{
库特
一个目标是不需要为每个事件定义类
这是一个很好的迹象,你想要的东西比C++更适合你的目的,因为它没有动态反射能力。(如果你使用更动态的东西,但仍然需要与C++接口,那么你需要弥补这个差距,所以这个答案可能还是不一定有用。)
现在,虽然可以构建(有限的)动态系统,但您应该问问自己,这是否是您真正想要做的。例如,如果您“关闭”事件及其回调签名的世界,您将保留大量类型安全性:
// 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…>;
//采用变量类型,例如Boost.variant
使用key\u callback=variant<
函数//代码
,函数//代码,用户
,函数//代码,用户,持续时间
,函数//用户
>;
使用callback_type=variant;
但是,本着坚持您的要求的精神,以下是如何存储任何†回调,并且仍然能够调用它:
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) { ... });
*/
使用any=boost::any;
使用arg_type=std::vector
如果你有一个类型特征可以猜测一个可调用类型的签名,你可以编写一个使用它的erase
(同时仍然允许用户在无法推断的情况下填写)。我在示例中不使用一个,因为那是另一个蠕虫
†:“any”表示任何可调用对象接受一定数量的可复制参数,返回void
——您可以通过使用类似于boost::any
的仅移动包装来放宽对参数的要求。我同意Luc的观点,即类型安全方法可能更合适,但以下解决方案确实如此或多或少是您想要的,但有一些限制:
参数类型必须是可复制的
参数总是被复制,从不移动
当且仅当fire()
的前N个参数的类型与处理程序的参数类型完全匹配时,才会调用具有N个参数的处理程序,并且不会执行隐式转换(例如,从字符串文字到std::string
)
处理程序不能是具有多个重载运算符()
的函子
这就是我的解决方案最终允许您编写的内容:
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);
}
而第二次调用将产生以下输出:
Got a 42
Got a 42 and a hi
Bad arity!
Bad argument!
Bad arity!
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!
这是一本书
该实现基于boost::any
。它的核心是dispatcher
functor。它的call操作符接受一个类型为擦除参数的向量,并将它们分派给构造它的可调用对象(处理程序)。如果参数类型不匹配,或者如果处理程序接受的参数多于提供的参数,则它只会将错误打印到标准输出,但如果您愿意,可以使其抛出,或者执行任何您喜欢的操作:
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;
};
很明显,如果处理程序是一个带有多个重载调用运算符的函子,那么整个过程将不起作用,但希望这个限制对您来说不会太严重
最后,event\u dispatcher
类允许您通过调用listen()
将类型擦除处理程序存储在多重映射中,并在使用适当的键和参数调用fire()
时调用它们(您的events
对象将是此类的实例):
struct事件调度器
{
公众:
模板
void listen(std::string const&event,F&&F)
{
_部署(事件,make_调度程序(std::forward(f));
}
模板
无效火灾(标准::字符串常量和事件,参数常量和…参数)
{
自动rng=\u回调。相等\u范围(事件);
用于(自动it=rng.first;it!=rng.second;++it)
{
调用(it->second,args…);
}
}
私人:
模板
无效调用(F常量和F、参数常量和…参数)
{
向量v{args…};
f(v);
}
私人:
std::multimap\u回调;
};
同样,整个代码都是可用的。这是完全可能的,实际上我有一个非常好的实现,只是有一些细微的差异。我有一些建议可能是相关的:1)字符串生成的键非常糟糕,因此除非您有非常具体的理由将字符串作为键,否则我建议您坚持使用某种类型的标记。好处通常是开销稍小,但首先是静态类型检查和捕获键入错误。2)这里的大多数逻辑错误都会在运行时被异常捕获,我不太明白每次调用信号调用抛出都有什么好处。不管怎样,我可以在明天的某个时候在办公室向您展示我的实现,它很灵活,支持将具有不同签名的lambda映射到信号,但使用静态标记进行调度。@Ylisar那太好了!当然,签名不匹配可以我知道,只有在实际触发事件时才会检测到。如果使用适当的类型实例化事件,则可以使其在最后两个事件上出现编译错误。基本思想是创建一个从X继承的类X,该类为该参数集定义一个函数,并具有触发器()函数,该函数将事件发送给其侦听器和基类。基本上,递归模板为每个参数集提供显式函数以支持&一个触发器函数来绑定它们。或者,将它们包装在lambda中,隐藏参数并存储在单个类中。@dascandy感谢您
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));
}
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...>;
};
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;
};