使用timax::bind, 来获取一个std::function对象 | 知行一

使用timax::bind, 来获取一个std::function对象

使用timax::bind, 来获取一个std::function对象

Motivation

刚刚发布的rest_rpc,接口易用性很高。例如服务器注册业务函数的接口:

我们希望将字符串”add”与业务函数client::add绑定。在示例中add是一个类型为int(int,int)的普通函数。除了普通函数,我们希望这个接口可以接受任何callable类型的对象,包括普通函数、仿函数、lambda表达式。这可以通过function_traits,可以很容易的达成。

但是如何兼容类的非静态成员函数?

使用std::bind?很遗憾,C++标准并没有明确定std:bind的返回值。更遗憾的是,std::bind返回的类型是一个拥有泛型operator()的结构体,也就是说,它的operator()是一个模板函数。因此,我们无法用function_traits做进一步的处理。而且适用std::bind,还有一个不便利的地方,我们在使用std::bind的时候,还需要填补placeholder.

于是,我们决定撸一个timax::bind,直接为用户返回std::function<Ret(Args…)>对象,这样可以让funtion_traits正常工作。

Design

什么该做,什么不该做

timax::bind并没有重新实现一个std::bind,而是封装并扩展了std::bind. 重复造轮子的工作不该做,而让std::bind在我们的应用场景下更好用是应该做的事情。timax::bind扩展的工作主要有两项:
1. 包装std::bind,为用户返回一个std::function对象;
2. 在函数参数都默认的情况下,不需要用户填上placeholder;

要做的工作

timax::bind的设计思路如下图:

我们用代码来详细解释一下

Implementation

timax::bind的实现,由上图可以看出,主要工作是两个方面。
1. 推导std::function<Ret(Args…)>中的Ret和Args…;
2. 返回一个可以构造std::function<Ret(Args…)>的对象;

推导参数主要看timax::bind是否绑定了参数,或者占位符。因为,绑定了额外的参数,会让函数的形参数目和顺序发生改变。在不绑定任何参数的情况下,是最简单的,同原来函数的返回类型与形参列表相同。

无额外绑定参数情形

正如上文所述,这种情况非常简单,因为返回类型很好确定。我们可以使用function_traits,推导出返回的std::function模板实例化后的类型。那么剩下的工作就是要构造一个对象,能够正确调用我们绑定的函数,并初始化std::function实例化后的对象实例。根据Design章节的附图,我们分成四种情况:
1. 绑定的是一个callable(函数、仿函数、lambda表达式和函数对象);
2. 用一个类对象绑定一个类的非静态成员变量;
3. 用一个类的裸指针绑定一个类的非静态成员变量;
4. 用一个类的智能指针绑定一个类的非静态成员变量;

绑定一个普通callable的实现很简单,我们用一个lambda表达式包装了一下调用。这里用到了C++14的auto lambda表达式,使用auto和variadic特性,让编译器自动帮我们填写函数的signature. 如果没有C++14的特性,还真不知道怎么实现比较方便,欢迎各路大神在这里指教一下,如何只用C++11的特性来实现。

剩下三种情况的实现方法,也很明了了,唯一要确定的是调用语义。而且,我们的实现是支持多态的。

最后利用模板元,特化和重载决议等一些modern C++的惯用法,把这些实现像用胶水一样粘起来,统一成一个接口给外层用就可以了。

有额外参数绑定的情形

有额外参数的情形就麻烦一些了,因为函数的形参列表发生了改变。绑定了实参,形参列表会变小;绑定了占位符,形参列表的顺序可能会改变。那么如何计算返回的std::function的实例化类型呢?切入点在placeholder!

也就是说,我们在计算返回类型与形参列表的类型的时候,只需要考虑placeholder而可以忽略所有实参。接下来继续用代码来描述计算类型额过程。

对于有绑定实参的情形,可以看出,我们只关心占位符placeholder,所以结果也完全正确。如果没有任何占位符,那么形参列表就是void. 在rest_rpc中,timax::bind_to_function模板就是负责推导std::function实例化类型的元函数,详细可以参见function_traits.hpp的151行。剩下的事情就是调用std::bind了,这就很简单了。

Some Tricks

在实现timax::bind的时候,有一些实用的C++小技巧,简化了我们的代码复杂度。在这一个小节跟大家分享一下。

判断一个对象是否是智能指针

因为智能指针不能像裸指针一样访问pointer-to-member-function,要先get()出裸指针,再访问。所以,我们要区分裸指针跟智能指针两个类型。判断一个类型是否是裸指针,直接实用std::is_pointer就能完成,但判断是否是智能指针,得自己实现。

智能指针都重载了operator->,并都有get接口访问裸指针,我们就用这两个条件来判断一个类型是否是智能指针,代码如下,这应该是当前C++版本最简单的SFINAE技巧:

如果T是智能指针,那么两次decltype就能够计算出表达式类型,那么voider就能够导出void,并生成is_smart_point<T, void>的特化,返回std::true_type. 否则,特化不存在,就导出std::false_type.

兼容boost::placeholders

兼容boost::placeholders是一个无奈之举,因为包含了boost/bind.hpp就会自动把_1和_2引入全局命名空间下,还会与标准库的占位符冲突。好在兼容的工作很简单,特化一下std::is_placeholder就能够解决了。

Tail

timax::bind的实现特别简单,但是为rest_rpc注册业务函数带来了便利。这就是C++的魅力,在语言工具和标准库功能不够的时候,你随时都有可能为自己定制和扩展C++. 定制和扩展势必会用到一些模板元和宏元,会写一些思路不同于常规程序的代码。但这些代码的出现,是为了让常规代码更符合人类直观感受的逻辑思维。看到一些人喜欢妖魔化C++的种种特性,甚至把回字的四种写法拿出来嘲讽这些代码是咬文嚼字,工笔对偶的华而不实。我只能说,不同的需求关心的问题大相径庭。在编写库的时候,需要实现者反复琢磨,接口是否符合常规逻辑思维,是否类型安全,是否是富类型能涵盖所有等等,才有了使用者顺滑的开发体验。《C++语言的设计与演化》诠释了BS对C++设计哲学的源点,不以个人喜好来增删语言的特性,所以我们才能看到一个强大的编程语言。然而新标准也出的忒慢了吧! 愿rest_rpc功能越来越完善,C++er越来越好,也欢迎各路大神来访purecpp.org和QQ群296561497一起讨论C++的开()发()经()验()。

《使用timax::bind, 来获取一个std::function对象》有2个想法

发表评论