分类目录归档:社区精华

扩展模板库的新方法 – 善用函数的ADL查找

问题

随着C++语言的发展,越来越多的C++类库都使用模板库提供给大家使用。模板库通常使用基于对象的设计,相比传统的面向对象的设计而言,前者的每个模块可以单独使用,而不依赖框架。整个模板库的框架,只是一个胶水层,让各个组件之间按照某种协议,组合到一起,形散而神不散,发挥出强大的功能。而且模板库非常好扩展。模板库的扩展通常有以下三种模式:

  1. 函数重载;
  2. Policy Based Design, 按照框架规定好的协议导出指定的type,计算好特定的静态常量和实现特定的函数;
  3. 特化traits或者一些特定模板组件的特化;

由于前两种方式比较简单,还要就是没有找到比较好的素材,鄙文暂时主要讨论第三种扩展方式。在实现iguana的过程中,我们发现模板特化和宏的结合,在碰到命名空间的时候,我们一筹莫展。案例如下,

REFELCTION宏实质上生成了一个在全局命名空间下的类模板的特化的代码。但是由于宏是预处理阶段,生成的代码在client命名空间下。C++的语言规范不允许在一个命名空间中特化另一个命名空间的类模板,也不允许在一个命名空间下特化全局命名空间的类模板(全局的情况MSVC能编过,我是服气的)。这里是一个参考链接。

那么我们库中遇到的问题,用下列简化的代码给出,有兴趣的读者可以到各个编译环境下测试。

其实这个限制并不是无解,只要你保证REFLEACTION宏在全局命名空间下,就不会出错。例如BOOST_FUSION_ADAPT_STRUCT宏,也是用相同的方法规避的。但是你会发现这样对使用来说非常不便,你偶尔不得不把一个完整的命名空间的代码block拆成两个。我们得找另外一种方法来扩展我们的模板库,而不受限于命名空间。

解决方案

什么样的扩展方法,可以跨越命名空间的限制?仔细阅读过template C++的读者应该不难发现,就是函数的ADL查找。我们用如下代码来说明,如何利用函数的ADL查找。

简单解释一下,下面的代码是如何工作的。首先,函数的ADL查找,是apply函数尝试调用to_extend的时候,不仅会查找lib命名空间下的符号,也会去查找T类型所在命名空间的符号。我们定义的to_extend函数在client命名空间下,foo类型也在client命名空间下。那么ADL查找肯定可以找到to_extend函数的符号。然后,我们没有选择类模板的特化,而是选择了使用to_extend函数,返回一个它内部定义的类型作为policy的功能。这个思想有点类似Andrei Alexandrescu大神的MDC中第11章节的Multimethod的蹦床函数,trampoline funciton. 有了这些,我们就可以在任意命名空间下来扩展我们的模板库,当然配合宏使用也没有问题了。在iguana中具体实现的小细节,可以参考reflection.hpp里面的实现。

The design philosophy of rest_rpc

The design philosophy of rest_rpc

Bellow are the design philosophy of rest_rpc:

  1. Rapid development;
  2. Easy to use;
  3. Focus on business;

Rapid development

You can develop a tcp network application in 5 minutes using rest_rpc. There is an example of a rpc server which provides some rpc service.
Firstly, define a business method ‘add’.

Secondly, register the business method as a rpc api for client.

Now You have finished a rpc server less than 10 lines code, the rpc server provide only one service.You can easily
add more rpc api as bellow:

1.define business logic:

2.register business logic:

Above example shows how to apply a business class for rpc service.
How about the client?The client code just need very little code,
Bellow is a rest_rpc client example:

1.define call protocol

2.directly call with parameters

You can develop a network application rapidly and convenient with rest_rpc, very little code is needed.

Easy to use

Easy to use is rest_rpc’s main goal, we do much effort for the goal. We hope both client and server are very easy to use.THe first principle is that RPC called just like local function calls.

At client we call a universal method ‘call’ which needs remote endpoint, RPC service name and parameters, it’s very like a local function call.We hope the user
can forget this is a network program, but treat it as a local application.

The second principle is omitted network and serialization details, because we want to use the interface as simple as we can, no extra afford.So rest_rpc do the fussy for users and they can’t feel it.

We verify RPC parameters are type correct at compile-time instead of just run-time, We can find problem as soon as possible.We utilize meta data to avoid defining a protocol file which can reduce learning costs.

At server we also do much work to simplify user interface. you can register callback of any signature with same manner, no restriction at all.

Focus on business

To be continued…

C++14实现编译期反射–剖析magic_get中的magic

pod类型编译期反射

在2016年的cppcon技术大会上,Antony Polukhin做了一个关于C++反射的演讲,他提出了一个实现反射的新思路,即无需使用宏、标记和额外的工具即可实现反射。这看起来似乎是一件不可能完成的任务,因为C++是没有反射机制的,无法直接获取对象的元信息。但是Antony Polukhin发现对pod类型使用modern c++的模版元技巧可以实现这样的编译期反射。他开源了一个pod类型的编译期反射库magic_get,这个库也准备进入boost。我们来看看magic_get的使用示例。

通过这个示例可以看到,magic_get确实实现了非侵入式访问foo对象的字段,不需要写任何宏、额外的代码以及专门的工具,直接在编译期就可以访问pod对象的字段,没有运行期负担,确实有点magic。

本文将通过分析magic_get源码来介绍magic_get实现的关键技术,深入解析实现pod类型反射的原理。

关键技术

实现pod类型反射的思路是这样的:先将pod类型转换为对应的tuple类型,接下来将pod类型的值赋给tuple,然后就可以通过索引去访问tuple中的元素了。所以实现pod反射的关键就是如何将pod类型转换为对应的tuple类型和pod值赋值给tuple。

1. pod类型转换为tuple类型

pod类型对应的tuple类型是什么样的呢?以上面的foo为例,foo对应的tuple应该是tuple<int, char>, 即tuple中的元素类型和顺序和pod类型中的字段完全一一对应。

根据结构体生成一个tuple的基本思路是,按顺序将结构体中每个字段的类型萃取出来并保存起来,后面再取出来生成对应的tuple类型。然而字段的类型是不同的,C++也没有一个能直接保存不同类型的容器,因此需要一个变通的方法,用一个间接的方法来保存萃取出来的字段类型,即将类型转换为一个size_t类型的id,将这个id保存到一个array<size_t, N>中,后面根据这个id来获取实际的type并生成对应的tuple类型。

这里需要解决的一个问题是如何实现类型和id的相互转换。

type和id在编译期相互转换

先借助一个空的模版类用来保存实际的类型,再借助C++14的constexpr特性,在编译期返回某个类型对应的编译期id,就可以实现type转换为id了。具体代码如下:

上面的代码在编译期将类型int和char做了一个编码,将类型转换为一个具体的编译期常量,后面就可以根据这些编译期常量来获取对应的具体类型。
编译期根据id获取type的代码如下:
constexpr auto id_to_type( std::integral_constant<std::size_t, 6> ) noexcept { int res{}; return res; }
constexpr auto id_to_type( std::integral_constant<std::size_t, 9> ) noexcept { char res{}; return res; }
上面的代码中id_to_type返回的是id对应的类型的实例,如果要获取id对应的类型还需要通过decltype推导出来。magic_get通过一个宏将pod基本类型都做了一个编码,以实现type和id在编译期的相互转换。

将类型编码之后,保存在哪里以及如何取出来是接着要解决的问题。magic_get通过定义一个array来保存结构体字段类型id。

array中的定长数组data中保存字段类型对应的id,数组下标就是字段在结构体中的位置索引。

萃取pod结构体字段

前面介绍了如何实现字段类型的保存和获取,那么这个字段类型是如何从pod结构体中萃取出来的呢?具体的做法分为三步:

  1. 定义一个保存字段类型id的array;
  2. 将pod的字段类型转换为对应的id,按顺序保存到array中;
  3. 筛除array中多余的部分;

下面是具体的实现代码:

定义array的时候需要定义一个固定的数组长度,这个数组的长度应该为多少合适呢?按照结构体最多的字段数来确定。因为结构体的字段数最多为sizeof(T),所以array的长度设置为sizeof(T)。array中的元素全部初始化为0。一般情况下,结构体字段数一般不会超过array的长度,那么array中就就会出现多余的元素,所以还需要将array中多余的字段移除,只保存有效的字段类型id。具体的做法是计算出array中非零的元素有多少,接着再把非零的元素赋给一个新的array。下面是计算array非零元素个数,同样是借助constexpr实现编译期计算。

由于字段是按顺序保存到array中的,所以在元素值为0时的count就是有效的元素个数。接下来我们来看看detect_fields_count_and_type_ids的实现,这个constexpr函数将结构体中的字段类型id保存到array的data中。

detect_fields_count_and_type_ids的第一个参数为定长数组array<std::size_t, sizeof(T)>的data,第二个参数是一个std::index_sequence整形序列。detect_fields_count_and_type_ids具体实现代码如下:

上面的代码是为了将index_sequence展开为0,1,2…, sizeof(T)的序列,得到这个序列之后,再调用type_to_array_of_type_ids函数实现结构体中的字段类型id保存到array中。
在讲type_to_array_of_type_ids函数之前我们先看一下辅助结构体ubiq。保存pod字段类型id实际上是由辅助结构体ubiq实现的,它的实现如下:

这个结构体比较特殊,我们先把它简化一下。

这个结构体的特殊之处在于它可以用来构造任意pod类型,比如int, char, double等类型。

因为ubiq构造函数所需要的类型由编译器自动推断出来,所以它能构造任意pod类型。通过ubiq结构体获取了需要构造的类型之后,我们还需要将这个类型转换为id按顺序保存到定长数组中。

上面的代码中先将编译器推导出来的类型转换为id,然后保存到数组下标为I的位置。
再回头看type_to_array_of_type_ids函数。

type_to_array_of_type_ids有两个模版参数,第一个T是pod结构体的类型,第二个size_t…为0到sizeof(T)的整形序列,函数的入参为size_t*,它实际上是array<std::size_t, sizeof(T)>的data, 用来保存pod字段类型id。

保存字段类型的关键代码是这一行:T{ ubiq{types}… },这里利用了pod类型的构造函数,通过initializer_list构造,编译器会将T的字段类型推导出来,并借助ubiq将字段类型转换为id保存到数组中。这个就是magic_get中的magic!

将pod结构体字段id保存到数组中之后,接下来就需要将数组中的id列表转换为tuple了。

pod字段id序列转换为tuple

pod字段id序列转换为tuple的具体做法分为两步:

  1. 将array中保存的字段类型id放入整形序列std::index_sequence;
  2. 将index_sequence中的类型id转换为对应的类型组成tuple。

下面是具体的实现代码:

get是返回数组中某个索引位置的元素值,即类型id,返回的id放入std::index_sequence中,接着就是通过index_sequence将index_sequence中的id转换为type,组成一个tuple。

id_to_type返回的是某个id对应的类型实例,所以还需要decltype来推导类型。这样我们就可以根据T来获取一个tuple类型了,接下来是要将T的值赋给tuple,然后就可以根据索引来访问T的字段了。

2. pod赋值给tuple

对于clang编译器,pod结构体是可以直接转换为std::tuple的,所以对于clang编译器来说,到这一步就结束了。

然而,对于其他编译器,如msvc或者gcc, tuple的内存并不是连续的,不能直接将T转换为tuple,所以更通用的做法是先做一个内存连续的tuple,然后就可以将T直接转换为tuple了。

内存连续的tuple

下面是实现内存连续的tuple代码:

base_from_member用来保存tuple元素的索引和值,tuple_base派生于base_from_member,自动生成tuple中每一个类型的base_from_member,tuple派生于tuple_base用来简化tuple_base的定义。再给tuple增加一个根据索引获取元素的辅助方法。

这样就可以通过get就可以获取tuple中的元素了。
到此,magic_get的核心代码分析完了。由于实际的代码会更复杂,为了让读者能更容易看懂,我选取的是简化版的代码,完整的代码可以参考github上的magic_get或者我github上简化版的代码

总结

magic_get实现了对pod类型的反射,可以直接通过索引来访问pod结构体的字段,而不需要任何额外的宏、标记或工具,确实很magic。magic_get主要是通过c++11/14的可变模版参数、constexpr、index_sequence、pod构造函数以及很多模版元技巧实现的。那么magic_get可以用来做些什么呢?根据magic_get无需额外的负担和代码就可以实现编译期反射的特点,很适合做ORM数据库访问引擎和通用的序列化/反序列化库,我相信还有更多潜力和应用等待我们去发掘。
modern c++的一些看似平淡无奇的特性组合在一起就能产生神奇的魔力,让人不禁赞叹modern c++蕴藏了无限的可能性与神奇!

本文发表于2017.3月《程序员》杂志,转载请注明出处。

如何设计一个简单的C++ ORM

如何设计一个简单的C++ ORM

注:本文是由社区的BOT Man写的,由我代为发上来。

2016/11/15

“没有好的接口,用C++读写数据库和写图形界面一样痛苦”

阅读这篇文章前,你最好知道什么是
Object Relation Mapping (ORM)

为什么C++要ORM

As good object-oriented developers got tired of this repetitive work,
their typical tendency towards enlightened laziness started
to manifest itself in the creation of tools to help automate
the process.

When working with relational databases,
the culmination of such efforts were object/relational mapping tools.

  • 一般的C++数据库接口,都需要手动生成SQL语句;
  • 手动生成的查询字符串,常常会因为模型改动而失效 (这里超级容易出错);
  • 查询语句/结果和C++原生数据之间的转换,每次都要手动解析;

市场上的C++ ORM

ODB

需要一个独立的 预编译器 生成模型和操作;

sqlpp11

使用 生成模型和操作,使用起来也比较复杂;
不过个人比较喜欢这个设计;

Hiberlite ORM

  • 需要在定义模型时,插入 额外的代码 进行注入;
  • 没有条件查询
    Transaction

C++的ORM要做什么

  • 将对C++对象操作转化成SQL查询语句
    (LINQ to SQL);
  • 提供C++ Style接口,更方便的使用;

一个可行的设计

一个实现了基本功能的C++ ORM —— ORM Lite

关于这个设计的代码和样例:

https://github.com/BOT-Man-JL/ORM-Lite

设计上大致分为6个方面:

  1. 封装SQL链接器
  2. 遍历对象内需要持久化的成员
  3. 序列化和反序列化
  4. 获取类名和各个字段的字符串
  5. 获取字段类型
  6. 将对C++对象的操作转化为SQL语句

1. 封装SQL链接器

为了让ORM支持各种数据库,
我们应该把对数据库的操作抽象为一个统一Execute

  • 因为SQLite比较简单,目前只实现了SQLite的版本;
  • MySql版本应该会在
    这里 维护。。。

2. 遍历对象内需要持久化的成员

2.1 使用 Visitor Pattern + Variadic Template 遍历

一开始,我想到的是使用
Visitor Pattern
组合
Variadic Template
进行成员的遍历;

首先,在模型处加入 __Accept 操作;
通过 VISITOR 接受不同的 Visitor 来实现特定功能;
并用 __VA_ARGS__ 传入需要持久化的成员列表

然后,针对不同功能,实现不同的 Visitor
再通过统一的 Visit 接口,接受模型变长数据成员参数;
例如 ReaderVisitor

  • Visit 将操作转发给带有变长模板_Visit
  • 变长模板_Visit 将各个操作转发给处理单个数据_Visit
  • 处理单个数据_Visit模型的数据
    Visitor 一个 public 数据成员( serializedValues)交换;

不过,这么设计有一定的缺点:

  • 我们需要预先定义所有的 Visitor,灵活性不够强;
  • 我们需要把和需要持久化的成员交换的数据保存到 Visitor 内部,
    增大了代码的耦合;

2.2 带有 泛型函数参数 的 Visitor

(使用了C++14的特性)

所以,我们可以让 Visit 接受一个泛型函数参数,用这个函数进行实际的操作;

模型处加入的 __Accept 操作改为:

  • fn泛型函数参数
  • 每次调用 __Accept 的时候,把 fn 传给 visitorVisit 函数;

然后,我们可以定义一个统一的 Visitor,遍历传入的参数,并调用 fn ——
相当于将 Visitor 抽象为一个 for each 操作:

最后,实际的数据交换操作通过传入特定的 fn 实现:

  • 对比上边,这个方法实际上是在处理单个数据_Visit模型的数据
    传给回调函数 fn
  • fn 使用
    Generic Lambda
    接受不同类型的数据成员,然后再转发给其他函数( DeserializeValue);
  • 通过capture需要持久化的成员交换的数据;

2.3 另一种设计——用 tuple + Refrence 遍历

(使用了C++14的特性)

虽然最后版本没有使用这个设计,不过作为一个不错的思路,我还是记下来了;

首先,在模型处通过加入生成 tuple 的函数:

  • forward_as_tuple__VA_ARGS__ 传入的参数转化为引用的 tuple
  • decltype (auto) 自动推导返回值类型;

然后,定义一个 TupleVisitor

  • 其中使用了 _SizeT 巧妙的进行 tuple 下标的判断;
  • 具体参考

    http://stackoverflow.com/questions/18155533/how-to-iterate-through-stdtuple

最后,类似上边,实际的数据交换操作通过 TupleVisitor 完成:

2.4 问题

  • 使用 Variadic Templatetuple 遍历数据,
    其函数调用的确定,都是编译时就生成的,这会带来一定的代码空间开销;
  • 后两个方法可能在 实例化Generic Lambda 的时候,
    针对 不同类型的模型的 不同数据成员类型 实例化出不同的副本,
    代码大小更大;

3. 序列化和反序列化

通过 序列化
将 C++ 数据类型转化为字符串,用于查询;
通过 反序列化,
将查询得到的字符串,转回 C++ 的数据类型;

3.1 重载函数 _Visit

针对每种支持的数据类型重载一个 _Visit 函数,
然后对其进行相应的序列化和反序列化

以序列化为例:

3.2 使用 std::iostream

然而,针对每种支持的数据类型重载,这种事情在标准库里已经有人做好了;
所以,我们可以改用了 std::iostream 进行序列化和反序列化

以反序列化为例:

4. 获取类名和各个字段的字符串

我们可以使用中的 # 获取传入参数的文字量;
然后将这个字符串作为 private 成员存入这个类中:

其中
#_MY_CLASS_类名
#__VA_ARGS__ 为传入可变参数的字符串;
__FieldNames 可以通过简单的字符串处理获得各个字段的字符串

5. 获取字段类型

新建数据库的 Table 的时候,
我们不仅需要类名和各个字段的字符串,
还需要获得各个字段的数据类型

5.1 使用 typeid 运行时判断

Visitor 遍历成员时,将每个成员的 typeid 保存起来:

运行时根据 typeid 判断类型并匹配字符串:

5.2 使用 &lt;type_traits&gt; 编译时判断

由于对象的类型在编译时已经可以确定,
所以我们可以直接使用 &lt;type_traits&gt; 进行编译时判断:

我们可以使用一个 Query 对象,专门处理条件查询;
并在其带有条件的操作中,返回自己的引用
从而实现Fluent Interface

6.2 自动将C++表达式转为SQL表达式

首先,引入一个 Expr 类,用于保存条件表达式;
Expr 对象传入 ORQuery.Where,以实现条件查询:

  • expr 保存了表达式序列,包括该成员的指针关系运算符 值的字符串;
  • 重载 &amp;&amp; || 实现表达式的复合条件

不过这样的接口还不够友好;
因为如果我们要生成一个 Expr 则需要手动传入 const std::string &amp;relOp

所以,我们在这里引入一个 Field_Expr 实现自动构造表达式

  • Field 函数用更短的语句,返回一个 Field_Expr 对象;
  • 重载 == != &gt; &lt; &gt;= &lt;= 生成带有对应关系的 Expr 对象;

6.3 自动判断C++对象的成员字段名

由于没有想到很好的办法,所以目前使用了指针进行运行时判断:

相当于使用 Visitor 遍历这个对象,找到对应成员的序号

6.4 问题

之后的版本可能考虑:

  • 支持更多的SQL操作,并改用语法树实现;
  • 提供Transaction;(欢迎 Pull Request)

写在最后

这篇文章是我的第一篇技术类博客,写的比较浅,见谅;

你有一个苹果,我有一个苹果,我们彼此交换,每人还是一个苹果;
你有一种思想,我有一种思想,我们彼此交换,每人可拥有两种思想。

如果对以上内容及ORM Lite有什么问题,
欢迎 指点 讨论

https://github.com/BOT-Man-JL/ORM-Lite/issues

Delivered under MIT License © 2016, BOT Man

使用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才是好用的RPC

什么样的RPC才是好用的RPC

现在RPC框架很多,但是真正好的RPC却是少之又少。那么什么是好的RPC,什么是不好的RPC呢,有一个评判标准吗?下面是我列举出来的衡量RPC好用与否的几条标准:

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

下面来分别解释这几条标准。

  • 标准1:真的像本地函数一样调用

    RPC的本质是为了屏蔽网络的细节和复杂性,提供易用的api,让用户就像调用本地函数一样实现远程调用,所以RPC最重要的就是“像调用本地函数一样”实现远程调用,完全不让用户感知到底层的网络。真正好用的RPC接口,他的调用形式是和本地函数无差别的,但是本地函数调用是灵活多变的。服务器如果提供和客户端完全一致的调用形式将是非常好用的,这也是RPC框架的一个巨大挑战

  • 标准2:使用简单,用户只需要关注业务即可

    RPC的使用简单直接,非常自然,就是和调用本地函数一样,不需要写一大堆额外代码,用户只用写业务逻辑代码,而不用关注框架的细节,其他的事情都由RPC框架完成。

  • 标准3:灵活,RPC调用的序列化方式可以自由定制

    RPC调用的数据格式支持多种编解码方式,比如一些通用的json格式、msgpack格式或者boost.serialization等格式,甚至支持用户自己定义的格式,这样使用起来才会更灵活。

RPC框架评估

下面根据这几个标准来评估一些国内外知名大公司的RPC框架,这些框架的用法在github的wiki中都有使用示例,使用示例代码均来自官方提供的例子。

谷歌gRPC

gRPC最近发布了1.0版本,他是谷歌公司用c++开发的一个RPC框架,并提供了多种客户端。

  • 协议定义

  • 服务器代码

  • 客户端代码

  • 评价

    gRPC调用的序列化用的是protocal buffer,RPC服务接口需要在.proto文件中定义,使用稍显繁琐。根据标准1,gRPC并没有完全实现像本地调用一样,虽然很接近了,但做不到,原因是RPC接口中必须带一个Context的参数,并且返回类型必须是Status,这些限制导致gRPC无法做到像本地接口一样调用。
    根据标准2,gRPC的使用不算简单,需要关注诸多细节,比如Context和Status等框架的细节。根据标准3,gRPC只支持pb协议,无法扩展支持其他协议。

    综合评价:70分。

百度sofa-pbRPC

sofa-pbRPC是百度用c++开发的一个RPC框架,和gRPC有点类似,也是基于protocal buffer的,需要定义协议。

  • 协议定义

  • 服务器端代码

  • 客户端代码

  • 评价

    sofa-pbRPC的使用并没有像sofa这个名字那样sofa,根据标准1,服务端的RPC接口比gRPC更加复杂,更加远离本地调用了。根据标准2,用户要做很多额外的事,需要关注框架的很多细节,比较难用。根据标准3,同样只支持pb协议,无法支持其他协议。

    综合评价:50分。

腾讯Pebble

腾讯开源的Pebble也是基于protocal buffer的,不过他的用法比gRPC和sofaRPC更好用,思路都是类似的,先定义协议。

  • 协议定义

  • 服务器端代码

  • 客户端代码

  • 评价

    Pebble比gRPC和sofa-pbrpc更好用,根据标准1,调用方式和本地调用一致了,接口中没有任何限制。根据标准2,除了定义协议稍显繁琐之外已经比较易用了,不过服务器在使用上还是有一些限制,比如注册服务的时候只能注册一个类对象的指针,不能支持lambda表达式,std::function或者普通的function。根据标准3,gRPC只支持pb协议,无法扩展支持其他协议。

    综合评价:75分。

apache msgpack-RPC

msgpack-RPC是基于msgpack定义的RPC框架,不同于基于pb的RPC,他无需定义专门的协议。

  • 服务器端代码

  • 客户端代码

  • 评价

    msgpack-RPC使用起来也很简单,不需要定义proto文件,根据标准1,客户端的调用和本地调用一致,不过,服务器的RPC接口有一个msgpack::rpc::request对象,并且也必须派生于base类,使用上有一定的限制。根据标准2,服务器端提供RPC服务的时候需要根据method的名字来dispatch,这种方式不符合开闭原则,使用起来有些不方便。根据标准3,msgpack-rpc只支持msgpack的序列化,不能支持其他的序列化方式。

    综合评价:80分。

总结

目前虽然国内外各大公司都推出了自己的RPC框架,但是真正好用易用的RPC框架却是不多的,这里对各个厂商的RPC框架仅从好用的角度做一个评价,一家之言,仅供参考,希望可以为大家做RPC的技术选型的时候提供一些评判依据。

现代C++函数式编程

概述

函数式编程是一种编程范式,它有下面的一些特征:

  • 函数是一等公民,可以像数据一样传来传去。
  • 高阶函数
  • 递归
  • pipeline
  • 惰性求值
  • 柯里化
  • 偏应用函数

C++98/03中的函数对象,和C++11中的Lambda表达式、std::function和std::bind让C++的函数式编程变得容易。我们可以利用C++11/14里的新特性来实现高阶函数、链式调用、惰性求值和柯理化等函数式编程特性。本文将通过一些典型示例来讲解如何使用现代C++来实现函数式编程。

高阶函数和pipeline的表现形式

高阶函数就是参数为函数或返回值为函数的函数,经典的高阶函数就是map、filter、fold和compose函数,比如Scala中高阶函数:

  • map

  • filter

  • fold

  • compose

上面的例子中,有的是参数为函数,有的是参数和返回值都是函数。高阶函数不仅语义上更加抽象泛化,还能实现“函数是一等公民”,将函数像data一样传来传去或者组合,非常灵活。其中,compose还可以实现惰性求值,compose的返回结果是一个函数,我们可以保存起来,在后面需要的时候调用。

pipeline把一组函数放到一个数组或是列表中,然后把数据传给这个列表。数据就像一个链条一样顺序地被各个函数所操作,最终得到我们想要的结果。它的设计哲学就是让每个功能就做一件事,并把这件事做到极致,软件或程序的拼装会变得更为简单和直观。
Scala中的链式调用是这样的:

用法和Unix Shell的管道操作比较像,|前面的数据或函数作为|后面函数的输入,顺序执行直到最后一个函数。

这种管道方式的函数调用让逻辑看起来更加清晰明了,也非常灵活,允许你将多个高阶函数自由组合成一个链条,同时还可以保存起来实现惰性求值。现代C++实现这种pipeline也是比较容易的,下面来讲解如何充分借助C++11/14的新特性来实现这些高阶函数和pipeline。

实现pipeline的关键技术

根据前面介绍的pipeline表现形式,可以把pipeline分解为几部分:高阶函数,惰性求值,运算符|、柯里化和pipeline,把这几部分实现之后就可以组成一个完整的pipeline了。下面来分别介绍它们的实现技术。

高阶函数

函数式编程的核心就是函数,它是一等公民,最灵活的函数就是高阶函数,现代C++的算法中已经有很多高阶函数了,比如for_each, transform.

这些高阶函数不仅可以接受Lambda表达式,还能接受std::function、函数对象、普通的全局函数,很灵活。需要注意的是,普通的全局函数在pipeline时存在局限性,因为它不像函数对象一样可以保存起来延迟调用,所以我们需要一个方法将普通的函数转换为函数对象。std::bind也可以将函数转化为函数对象,但是bind不够通用,使用的时候它只能绑定有限的参数,如果函数本身就是可变参数的就无法bind了,所以,这个函数对象必须是泛化的,类似于这样:

上面的函数对象内部包装了一个普通函数的调用,当函数调用的时候实际上会调用普通函数globle_func,但是这个代码不通用,它无法用于其他的函数。为了让这个转换变得通用,我们可以借助一个宏来实现function到functor的转换。

我们先定义了一个宏,这个宏根据参数来生成一个可变参数的函数对象,这个函数对象的类型名为tfn_加普通函数的函数名,之所以要加一个前缀tfn_,是为了避免类型名和函数名重名。define_functor_type宏只是定义了一个函数对象的类型,用起来略感不便,还可以再简化一下,让使用更方便。我们可以再定义一个宏来生成转换后的函数对象:

make_globle_functor生成了一个可以直接使用的全局函数对象,使用起来更方便了。用这个方法就可以将普通函数转成pipeline中的函数对象了。接下来我们来探讨实现惰性求值的关键技术。

惰性求值

惰性求值是将求值运算延迟到需要值时候进行,通常的做法是将函数或函数的参数保存起来,在需要的时候才调用函数或者将保存的参数传入函数实现调用。现代C++里已经提供可以保存起来的函数对象和lambda表达式,因此需要解决的问题是如何将参数保存起来,然后在需要的时候传给函数实现调用。我们可以借助std::tuple、type_traits和可变模版参数来实现目标。

上面的测试代码中,我们先把参数保存到一个tuple中,然后在需要的时候将参数和函数f传入tuple_apply,最终实现了f函数的调用。tuple_apply实现了一个“魔法”将tuple变成了函数的参数,来看看这个“魔法”具体是怎么实现的。

tuple_apply_impl实现的关键是在于可变模版参数的展开,可变模版参数的展开又借助了std::index_sequence

运算符operator|

pipeline的一个主要表现形式是通过运算符|来将data和函数分隔开或者将函数和函数组成一个链条,比如像下面的unix shell命令:

C++实现类似的调用可以通过重载运算符来实现,下面是data和函数通过|连接的实现代码:

除了data和函数通过|连接之外,还需要实现函数和函数通过|连接,我们通过可变参数来实现:

其中fn_chain是一个可以接受任意个函数的函数对象,它的实现将在后面介绍。通过|运算符重载我们可以实现类似于unix shell的pipeline表现形式。

柯里化

函数式编程中比较灵活的一个地方就是柯里化(currying),柯里化是把多个参数的函数变换成单参数的函数,并返回一个新函数,这个新函数处理剩下的参数。以Scala的柯里化为例:

  • 未柯里化的函数

  • 柯里化之后

currying之后add(1)(2)等价于add(1,2),这种currying默认是从左到右的,如果希望从右到左呢,然而大部分编程语言没有实现更灵活的curring。C++11里面的std::bind可以实现currying,但要实现向左或向右灵活的currying比较困难,可以借助tuple和前面介绍的tuple_apply来实现一个更灵活的currying函数对象。

从测试代码中可以看到这个currying函数对象,既可以从左边currying又可以从右边currying,非常灵活。不过使用上还不太方便,没有fn(1)(2)(3)这样方便,我们可以通过运算符重载来简化书写,由于C++标准中不允许重载全局的operater()符,并且operater()符无法区分到底是从左边还是从右边currying,所以我们选择重载<<和>>操作符来分别表示从左至右currying和从右至左currying。

有了这两个重载运算符,测试代码可以写得更简洁了。

curry_functor利用了tuple的特性,内部有两个空的tuple,一个用来保存left currying的参数,一个用来保存right currying的参数,不断地currying时,通过tuple_cat把新currying的参数保存到tuple中,最后调用的时候将tuple成员和参数组成一个最终的tuple,然后通过tuple_apply实现调用。有了前面这些基础设施之后我们实现pipeline也是水到渠成。

pipeline

通过运算符|重载,我们可以实现一个简单的pipeline:

这个简单的pipeline虽然可以实现管道方式的链式计算,但是它只是将data和函数通过|连接起来了,还没有实现函数和函数的连接,并且是立即计算的,没有实现延迟计算。因此我们还需要实现通过|连接函数,从而实现灵活的pipeline。我们可以通过一个function chain来接受任意个函数并组成一个函数链。利用可变模版参数、tuple和type_traits就可以实现了。

测试代码中用一个fn_chain和运算符|将所有的函数组合成了一个函数链,在需要的时候调用,从而实现了惰性求值。

fn_chain的实现思路是这样的:内部有一个std::tuple

在调用call_impl的过程中,将std::index_sequence不断展开,先从tuple中获取第I个function,然后调用获得第I个function的执行结果,将这个执行结果作为下次调用的参数,不断地递归调用,直到最后一个函数完成调用为止,返回最终的链式调用的结果。

至此我们实现具备惰性求值、高阶函数和currying特性的完整的pipeline,有了这个pipeline,我们可以实现经典的流式计算和AOP,接下来我们来看看如何利用pipeline来实现流式的mapreduce和灵活的AOP。

实现一个pipeline形式的mapreduce和AOP

前面的pipeline已经可以实现链式调用了,要实现pipeline形式的mapreduce关键就是实现map、filter和reduce等高阶函数。下面是它们的具体实现:

这些高阶函数还需要转换成支持currying的functor,前面我们已经定义了一个普通的函数对象转换为柯里化的函数对象的方法:

通过下面这个宏让currying functor用起来更简洁:

我们定义了map、reduce和filter支持柯里化的三个全局函数对象,接下来我们就可以把它们组成一个pipeline了。

上面的例子实现了pipeline的mapreduce,这个pipeline支持currying还可以任意组合,非常方便和灵活。

有了这个pipeline,实现灵活的AOP也是很容易的:

上面的测试例子中,核心逻辑是func函数,我们可以在func之前或之后插入切面逻辑,切面逻辑可以不断地加到链条前面或者后面,实现很巧妙,使用很常灵活。
总结

本文通过介绍函数式编程的概念入手,分析了函数式编程的表现形式和特性,最终通过现代C++的新特性和一些模版元技巧实现了一个非常灵活的pipeline,展示了现代C++实现函数式编程的方法和技巧,同时也提现了现代C++的强大威力和无限的可能性。文中完整的代码可以从我的GitHub上查看。

本文的代码和思路参考和借鉴了这篇文章,在此表示感谢。

注:本文是作者发表在程序员8月刊上的文章,转载请注明出处。

Kapok发布1.0版本了

kapok1.0发布了

kapok的特点

kapok是一个高性能跨平台的对象-json序列化的库,对象序列化后是标准的json格式,json格式的字符串可以直接反序列化为对象。简单,易用,header-only,只需要引用Kapok.hpp即可。它由c++14实现,因此需要支持C++14的编译器。

kapok除了支持结构体、容器和数组之外,还支持boost.optional和boost.variant,使用起来非常方便。

性能对比

kapok序列化和反序列化的性能很高,综合性能比msgpack要高,下面是性能对比的代码

综合测试

测试msgpack

测试kapok

测试结果

单项测试

分别测试msgpack序列化和反序列化的耗时

分别测试kapok的序列化和反序列化的耗时

测试结果

测试结论

从单项测试结果看,msgpack的序列化比kapok快大约2-3倍,而kapok的反序列化速度比msgpack快了5倍,综合性能,kapok比msgpack快4-5倍。

使用示例

  • 普通对象

  • 枚举对象

  • std::pair

  • std::array

  • std::tuple

  • boost.optional

  • boost.variant

  • 嵌套的容器和对象

需要注意的地方

  • kapok不支持指针,当序列化的对象中含有指针的时候会出现编译错误。

  • 反序列化的时候,如果反序列化和序列化的字段类型不匹配的时候,kapok会忽略该字段,不会赋值。

  • 序列化/反序列化过程中kapok可能会抛出异常,用户需要在外面捕捉异常。

  • kapok不是线程安全的,如果需要多线程操作的话,请用户在外面自行加锁。

  • 序列化的对象一定要定义META宏,否则会出现编译错误。

rest rpc发布预览版了!

Motivation

目前传统的c++ RPC框架一般都是基于protobuf或者是thrift,都需要用专门的代码生成器来生成代码,这种方式存在以下这些问题:

  • 使用麻烦。使用时需要先写一个DSL描述文件,然后用代码生成器来生成代码,如果model类很多的时候,工作就很繁琐,工作量也比较大。
  • 维护性差。当某些model类需要修改时,必须重新定义和编译,做一些繁琐而重复的工作。
  • 学习成本高。使用它们之前先要学习代码生成器如何使用,还要学习复杂的DSL语法定义规则,而这些语法规则并不是通用的,一段时间不用之后又要重新去学习。
  • 不能快速响应API升级的需求。当API或者协议演进的时候,就不得不让客户更新SDK。比如,当多语言的客户端较多时,每加一个接口时都要更新一堆不同语言的SDK,这是升级维护的噩梦。

面对这些问题,rest_rpc就应运而生了,她就是来解决这些问题的,rest_rpc的主要特性:

  • 简单、好用、灵活
  • 让用户只关注于业务,业务之外的事由框架负责
  • 不需要学习和编写DSL
  • 不需要使用代码生成器
  • 能快速响应API升级的需求
  • 彻底消除了繁琐重复的model定义工作
  • modern(c++14)
  • 跨平台

Getting started

  • 从github上下载rest_rpc git clone https://github.com/topcpporg/rest_rpc.git
  • 下载依赖的库Kapokspdlog git submodule update –init
  • 下载boost库,需要boost1.55以上版本
  • 编译 需要支持C++14的编译器,gcc4.9以上,vs2015以上, linux下直接使用cmake编译CMakelists.txt,win下直接使用vs2015打开restrpc.vcxproj

Tutorial

服务端代码

rpc server提供两个服务,一个是add服务,实现一个简单的加法;一个是translate服务,实现将字符串从小写转换为大写。

rpc server需要做的事情很简单,它只需要定义rpc服务接口即可,这也是rest rpc的设计理念,让用户只专注于业务。业务接口几乎不受限制,可以是普通函数,也可以是函数对象或者成员函数,函数的参数可以是基本类型也可以是结构体,灵活好用。需要注意的是函数参数不能是指针类型。

客户端代码

rpc client连接到服务器之后,直接调用通用接口既可以完成rpc调用,调用方式接近本地调用。