functor map
之前有写过一个简单的信号和槽的实现,但这个实现的缺点是很明显的,不同类型的信号需要不同的实例。
今天,重新想了一下,尝试写了些实现,由于C++不支持反射,没办法在运行时动态的生成可调用对象,所以,比如下面的做法是不可能的:
Functor functor;
functor.bind("add", [](int lhs, int rhs) -> int { return lhs + rhs; });
functor.bind("concat", [](const std::string& lhs, const std::string& rhs) -> std::string { return lhs + rhs; });
std::cout << functor.call("add", 1, 2) << '\n'; // print 3
std::cout << functor.call("concat", "hello ", "world") << '\n'; // print "hello world"
虽然知道不可能像上面代码那样,但是可以通过类型擦除,尽量做到和上面的类似。所以下面是具体的实现:
namespace nm
{
class FunctorMap
{
public:
template<typename> struct Inspector;
template<typename R, typename... Args>
struct Inspector<R(Args...)>
{
using arg_t = std::tuple<Args...>;
using res_t = R;
};
template<typename R, typename... Args>
struct Inspector<R(*)(Args...)> : Inspector<R(Args...)> {};
template<typename R, typename Object, typename... Args>
struct Inspector<R(Object::*)(Args...)> : Inspector<R(Args...)> {};
template<typename R, typename Object, typename... Args>
struct Inspector<R(Object::*)(Args...) const> : Inspector<R(Args...)> {};
template<typename R, typename Object, typename... Args>
struct Inspector<R(Object::*)(Args...) volatile> : Inspector<R(Args...)> {};
template<typename R, typename Object, typename... Args>
struct Inspector<R(Object::*)(Args...) const volatile> : Inspector<R(Args...)> {};
// functor like
template<typename Lambda>
struct Inspector : Inspector<decltype(&Lambda::operator())> {};
template<typename Lambda>
struct Inspector<Lambda&> : Inspector<decltype(&Lambda::operator())> {};
template<typename Lambda>
struct Inspector<Lambda&&> : Inspector<Lambda&> {};
public:
using Fp = std::function<void(void*, void*)>;
FunctorMap() = default;
template<typename F>
void bind(const std::string& sig, F&& f)
{
auto fp = [f = std::forward<F>(f)](void* args, void* res) {
using arg_t = typename Inspector<F>::arg_t;
using res_t = typename Inspector<F>::res_t;
using indices = std::make_index_sequence<std::tuple_size<arg_t>::value>;
arg_t& arg = *static_cast<arg_t*>(args);
*static_cast<res_t*>(res) = invoke<res_t, arg_t>(f, arg, indices{});
};
functors_[sig] = std::move(fp);
}
template<typename R, typename... Args>
R call(const std::string& sig, Args&&... args)
{
auto params = std::make_tuple(std::forward<Args>(args)...);
auto& fp = functors_[sig];
R res;
fp(¶ms, &res);
return res;
}
private:
std::map<std::string, Fp> functors_;
template<typename R, typename TupleType, typename F, size_t... Is>
static R invoke(F&& f, TupleType& args, std::index_sequence<Is...>)
{
return std::forward<F>(f)(std::forward<std::tuple_element_t<Is, std::remove_reference_t<TupleType>>>(std::get<Is>(args))...);
}
};
}
代码中的Inspector
直接取自variant
中的extract
,针对可变模板参数稍作修改。
用一个map
存放函数id和擦除类型后的functor
。通过模板参数指定call
的返回值,那么调用可以这样:
非常幸运,这段代码是可以编译通过并得到正确结果的。
接着,尝试一个字符串拼接
如你所见,失败了,并且抛出了一个异常,而且这个异常是由new
产生的。
注意到,在绑定functor
的时候的这段代码:
arg_t& arg = *static_cast<arg_t*>(args);
对于绑定为const std::string&
调用为const char*
的这种情况,这里的args
的真实类型是
tuple<const char*, const char*>
而arg_t
则是
tuple<const std::string&, const std::string&>
这里强行将前一个转换为后一个,如果直接赋值的话,会经过构造函数,就不会出现问题。而这里,完全不知道void* args
中args
的真实类型(根据call
的传入参数决定)。
所以,为了避免发生错误,可以要求用户使用绑定时的类型进行调用,同时还需要修改代码
// change
arg_t = tuple<Args...>;
// to
arg_t = tuple<remove_cv_t<remove_reference_t<Args>>...>;
至少需要删除const
和volatile
。
再次尝试运行
如你所见,正常了。
突然Déjà vu了,这个坑似乎之前就遇到过。。。
你也许会认为这样刻意模仿第一段代码的行为有什么意义,而且还加了各种限制。如果你细心的话会发现,这是个rpc
项目中的例子,所以并不会有call<int>("add", 1, 2)
这种需要返回值的情况,并且调用所需的参数都是传输数据结构(Json、MsgPack等)反序列化出来的,也就是说,反序列化后的字符串也不会是const char*
。
现有问题:
- 如果不需要返回值怎么办? (tag dispatch)
- 其它类型的functor呢?
- 没找到bind的functor怎么办?
- 传入参数是引用呢? (不支持引用,但你可以使用指针)
- 调用时的参数个数或者类型与bind时不一致会出现未定义行为 (2019-01-08增加,待解) 解法源于这里