使用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++的开()发()经()验()。

真正好用的RPC框架rest_rpc正式发布第一个版本

rest_rpc是由c++开源技术社区(purecpp.org)创建和发起的项目,在经过多次迭代和重构之后,终于发布第一个版本了。rest_rpc是modern c++开发的一个易用、灵活、跨平台和高性能的RPC框架。和国内外一些大公司开发的RPC框架相比,rest_rpc有哪些特色呢?

rest_rpc的特点

rest_rpc具备下面几个特点

  • 真的像本地函数一样调用
  • 使用简单,用户只需要关注业务即可
  • 灵活,RPC调用的序列化方式可以自由定制,比如支持json,支持msgpack等方式
  • 支持同步和异步调用

这几个特点也是之前的文章里提到的评价一个RPC是否好用的标准,无疑rest_rpc完全符合这些标准,是一个真正好用的RPC,并且还走得更远。

传统的网络库处理业务逻辑的过程一般分为5步:

  1. 接收网络数据;
  2. 解析网络数据;
  3. 调用业务逻辑;
  4. 打包结果;
  5. 发送数据;

如果使用rest_rpc,就只有1步了

1.只需要调用业务逻辑(其他的框架都帮你做好了)。

rest_rpc提供一站式服务,将1,2,4,5步完全省略掉,让用户只用关注第3步的业务逻辑即可,省心省力!如果用户之前用到了其他的网络库,想换成rest_rpc也很简单,不需要做任何修改,只要把业务逻辑函数注册一下就行了,可以直接复用,什么都不用改,省心省力!

rest_rpc的最主要的特点是好用,用户只需要像本地调用那样去调用RPC服务接口,无需关注框架和网络的细节既可以实现远程调用,只需要关注自己的业务逻辑即可。除了易用的特点之外,rest_rpc还具备很好的灵活性,用户可以选择RPC序列化的方式,还支持自定义的序列化方式。

rest_rpc的使用

我们以一个最简单的例子来展示如何使用rest_rpc,这个例子中,服务器提供了一个 int add(int a, int b) RPC服务接口,客户端通过RPC调用获取远程调用的结果。

  • 服务器端代码

  • 同步客户端代码

至此,一个RPC程序就完成了,无论是服务器还是客户端,代码都非常少,总共都不到10行代码,用户只需要关注业务逻辑即可,无需关注网络或者框架细节,而且和调用本地函数一样,非常好用,没有任何限制。

  • 异步客户端代码

一个更复杂的例子

这个例子将展示RPC接口中含有二进制数据的情况,有些RPC框架如果要支持二进制的话,需要将二进制做一些转换,比如base64转换之类的,rest_rpc支持原始的二进制数据,无需做任何转换。

  • 服务器端代码

  • 客户端代码

使用方式还是那么简单,自然,因为rest_rpc框架已经帮你做了绝大部分事情了。

rest_rpc编译

rest_rpc是由c++14编写的,因此需要支持C++14的编译器,windwos上需要vs2015, linux需要gcc5.0+, 除此之外还用到了boost,因此还需要boost库。

RPC调用需要注意的地方

需要注意的地方主要是就是客户端需要做异常处理,因为RPC调用可能会失败,出错的原因比较多,可能是客户端和服务器的连接断开了,也可能是服务器没有提供这个RPC服务,也可能是服务器提供的RPC服务发生了异常。总之,rest_rpc框架会将错误码和出错信息作为异常抛出来。所以更完整的做法是在call之外捕获一下异常,做异常处理。

此外,服务器在默认情况下是在io线程中执行业务函数的,如果用户需要执行一个非常耗时的操作,rest_rpc提供了一个异步执行业务函数的接口。

异步客户端

同步客户端会阻塞调用call的线程,虽然简化了逻辑但是也降低了性能。rest_rpc也实现了异步客户端,接口也很好用。

  • 异步客户端示例

  • 异步客户端同步接口
    异步客户端除了纯异步以外,还有同步接口,可以让用户选择在何时阻塞。

性能测试

rest_rpc的性能很高,下面是用异步客户端对add RPC服务接口做的性能测试结果,因为RPC是请求-响应模式,所以实际上做的是含有业务逻辑的pingpang测试,包括数据解包、业务执行、结果打包发送的过程。

上面是在一台12核(主频2.4G)24线程的服务器上测试的,qps为46万时,cpu占用63%左右。

代码质量

下面是用工具检测的代码质量图

代码的可读性较好。

如果你仅仅需要RPC的话,看到这里就可以不用往下看了

如果你还有更多期待,请往下看。

还有点其他的什么吗?

是的,还有一些特别的东西,rest_rpc不是仅仅提供了一个RPC功能而已,还提供了更有趣的功能,比如订阅-发布!是的,你没看错,rest_rpc具备pub/sub功能,也许有人会觉得奇怪,为什么RPC框架会提供订阅-发布功能呢。其实,RPC和订阅-发布是有相通的地方。RPC可以看作是一个特殊的订阅-发布模式,即订阅者和发布者都是自己,而订阅-发布模式又可以看作是一个特殊的扩展了的RPC,即发起RPC调用的人和接收RPC调用结果的人是不同的人。正是看到了这种相通性,rest_rpc顺手就实现了订阅-发布模式。订阅-发布模式用起来也很简单,和RPC调用差不多,下面来看一个订阅发布的例子。

  • 服务器端代码

  • pub客户端代码

鉴于pub和sub天然的异步属性,我们只在异步客户端实现了这个接口,同步客户端暂不支持

  • sub客户端代码

订阅发布还是那么简单。rest_rpc相比其他的RPC框架,不仅仅提供了更加易用、灵活的RPC接口,还提供了额外的订阅发布功能,而且订阅-发布可以和RPC调用随时结合起来使用,使得RPC和订阅-发布的功能更加强大。

根据value来获取tuple中的索引

根据值获取索引

测试结果

Copy Protected by Chetan's WP-Copyprotect.