分类目录归档:社区精华

Feather–一个快速开发的web服务器框架

Feather是一个适合快速开发的modern c++ web框架,Feather的目标是让使用者以最小的精力和成本来开发一个web网站。

现在很多web框架都非常庞大,学习成本高,而Feather正是为了解决这些问题的,它就像它的名字feather那样轻盈,所有的一切都是为了让用户非常方便又快速地开发而不是陷入到框架的细节当中。

一个好的框架应该是能让用户轻松实现自己的想法而不是成为框架的奴隶。如果你希望轻松快速地开发一个web网站,而无需花费大量的精力去学习框架细节的话,那么Feather非常适合你!

Feather是什么?

Feather作为一个快速开发框架,它的目标是让web开发变得简单。它主要有下面几个特点:

  1. 简洁易用
  2. 高性能,modern c++(c++17)开发
  3. header only
  4. 跨平台
  5. 支持编译期反射
  6. 支持AOP

Feather框架的核心包括:
1. 一个易用的http库,支持http1.x, https, websocket
2. 一个功能强大的html模版引擎
3. 一个可扩展的ORM库,支持多种数据库(mysql,postgresql,sqlite)
4. 一个可扩展序列化库,支持多种格式(json, xml)

Feather的架构

下面是Feather的架构图:

Feather的架构图

Feather内部的四个核心组件是松耦合的,Feather只是把它们像搭积木一样搭在一起。

  1. http组件: cinatra
  2. ORM组件: ormpp
  3. 序列化组件: iguana
  4. html 模版: inja

Feather的使用

以一个简单的例子展示如何使用Feather,比如显示文章列表,几乎每个网站都有这个功能。

获取文章列表的功能很简单,它底层数据部分是一个文章表,业务逻辑层是获取这些列表,然后做html渲染。对外接口部分是一个http服务,前端后端交互的数据格式为json。为了简单起见就不考虑缓存之类的。

那么要实现这个文章列表功能用Feather怎么做呢?可以按照这些步骤去开发:

  1. 提供获取文章列表的http接口;
  2. 通过ORM提供数据库访问功能;
  3. 编写业务逻辑,根据用户请求获取文章列表并通过html模版做渲染;

接下来看看这些步骤具体是怎么做的。

获取文章列表的http接口

其中login接口是这样的:

接下来就可以测试这个http服务接口了,客户端只要发送一个http请求即可。比如发送一个这样的请求

http://127.0.0.1/get_article_list

服务器会自动路由到article_controller::get_article_list函数中,如果请求不对则会返回http错误给客户端。当服务器收到这样的请求之后就表明服务器提供的这个http服务是可用的。

接下来需要编写数据库部分的代码,由于有了ORM,所以你可以很方便地编写数据库部分的代码了,同样很简单。

通过ORM提供数据库访问功能

登录业务涉及到一个用户表,因此我们需要创建这个表,不过在创建数据库之前先确定你选用什么数据库,Feather的ORM支持mysql, postgresql和sqlite三种数据库,假设我们的数据库是mysql。我们可以通过下面的代码来创建一个用户表。

1.创建文章表

dao.create_table< article >将会在testdb数据库中自动创建一个article表,其中id字段是自增长的。

2.编写获取文章列表的逻辑(包含访问数据库)

访问数据库,序列化为json返回给客户端。

详细的例子你可以看github上的代码

Demo示例

我们用Feather开发了一个社区网站,地址在这里:http://120.78.135.113:8080/

Feather社区

致谢

Feather社区网站由我和网友XMH共同开发完成(花了两天的业余时间),XMH也是cinatra的忠实用户,不但贡献了很多代码,还提供了很多宝贵的建议,在此表示衷心的感谢!

XMH是一名热爱编程的程序猿,平时从事c++,web,移动端等开发。从事过游戏后台和APP开发。也是个忠实的mordern c++粉丝,追随着cpp的发展,喜欢通过元编程做一些小工具。热爱开源社区,也是开源项目feather的使用者,希望feather能为更多的开发者所使用,社区发展越来越好。

希望有更多人能加入进来把Feather完善得更好。

联系我们

purecpp@163.com

http://purecpp.org/

https://github.com/qicosmos/feather

推荐下本人的无栈协程库librf

https://github.com/tearshark/librf

librf

librf – 协程库

librf是一个基于C++ Coroutines提案 ‘Stackless Resumable Functions’编写的非对称stackless协程库。

目前仅支持:
Windows (使用2017编译)(由于使用了SFINAE导致不再支持VS2015)

librf有以下特点:

  • 1.基于C++17提案’Stackless Resumable Functions’编写的非对称stackless协程库,可以以同步的方式编写简单的代码,同时获得异步的性能
  • 2.理论上支持海量协程, 创建100万个协程只需使用物理内存
  • 3.提供协程锁(mutex), 定时器, channel等特性, 帮助用户更加容易地编写程序
  • 4.可以很好的跟asio,libuv等库结合,能跟现有的callback范式的异步/延迟代码很好的结合
  • 5.目前还处于实验状态,不对今后正式的C++ Coroutines支持有任何正式的承诺

  • 如果你发现了任何bug、有好的建议、或使用上有不明之处,可以提交到issue,也可以直接联系作者:
    email: tearshark@163.net QQ交流群: 296561497

  • doc目录下有作者搜集的一些关于C++协程的资料

  • tutorial目录下有针对每个特性的范例代码,让用户可以循序渐进的了解librf库的特性

一个更好的自动注册工厂

在几年前我介绍过一种C++11实现的自动注册工厂,这是工厂模式的一种优雅的实现。在这里我们需要明确一个概念就是工厂模式,它是如何优雅地解决一个产品族的创建问题。所谓产品族就是一个继承体系的产品,比如有一个产品Message,它是一个基类,有很多Message是从它派生而来的,比如有Message1,Message2,Message3…等很多产品。

这些产品的创建依赖于某个key,类似于这样:

这是一个典型的工厂方法,这种写法在产品不多的时候是没问题的,但是如果产品越来越多的时候,switch-case就会越来越长,导致难以维护。另外还存在一个问题,有的产品不是无参数的构造函数,如果有些产品依赖了不同的参数,那么这个工厂方法是无法满足需求的。

之前介绍的自动注册工厂解决了switch-case膨胀的问题,但是对于需要参数的产品的创建没有解决得很好,需要进一步改进。改进的办法是把参数作为factory的模板参数,这样就可以解决有参数需求的问题了。

下面是具体实现:

这是一个Messgae产品族的工厂类,有了这个类之后我们就可以很方便地创建各种产品了。下面是测试例子:

Message产品族有4类产品,有的是无参的,有的是多个参数的产品,现在都可以统一创建了,直接输入key和构造参数即可,这个key可以自行修改为int或者枚举变量。

需要注意的是msg4,因为它注册的时候提供了一个function,让function提供创建功能,以满足更灵活地需求。

有了这样一个工厂类之后我们就可以很好地解决产品族创建的问题了。你还可以基于此把它改成一个抽象工厂类,但我觉得你应该慎重考虑一下是否有必要,一般情况下工厂模式就够了,不需要引入更多的复杂性。

tensorflow variant源码分析

tensorflow variant基本语义

通过分析tf.variant的源码可以知道它其实是一个any语义,即这个类型可以被任意类型赋值,它的主要目的就是做彻底的类型擦除。

这个名字取得有迷惑性,它和标准库和boost库中的variant语义是不一样的,而是和c++17中的std::any对应的。

tf.variant用法

可以从tf的测试代码中知道它的基本用法,用法很简单:

从这个测试代码中可以看到tf.variant和std::any/boost.any用法是差不多的。

tf.variant内部有一个unique_ptr指针, 默认为nullptr,所以没有赋值的时候总是返回nullptr。

Int(42)赋值给variant之后,就可以通过get(x)来获得Int指针了,接着就可以得到其实际的Int值了;因为这个Variant是any语义,所以任意类型比如Tensor也可以复制给它,取值方法也是类似的,先取T指针,接着调用value得到初始值。

注意,这里的get中类型必须和赋值时候的类型一致,不一致的时候会返回nullptr(除了void,传void时会返回void*指针);

any和variant比较

之前介绍过vairant,知道它也是用来做类型擦除的,不过variant擦除的类型是有限个的,必须要事先指定,它只能实现部分的类型擦除。

而any也是实现类型擦除的,它可以代表任何类型,不需要像variant那样需要事先确定擦除的类型,看起来更方便更强大了。

事实上这个any并不是变得更强大了,它虽然能彻底擦除类型,但是,在取值的时候需要知道准确的类型,这反倒不如variant方便了,variant在取值的时候通过一个visitor是不需要知道具体类型的。

二者的本质区别是类型擦除方式不一样,variant是通过栈上的一个定长内存块去保存赋值对象的,variant中所有的类型共用这块buffer,而any是通过派生,在堆内存上创建一个实际的对象,并且每次赋值都会析构之前的对象,重新在堆内存上重新创建一个新对象。

所以从性能上说variant的效率是高于any的,二者效率大约相差4倍左右。 附一个测试代码:

所以能用variant就不用any,除非真的需要擦除所有的类型,或者是为了追求代码更简单不太在意性能的时候可以用any,大部分情况下用variant擦除部分类型就可以满足需求了。

any的一个典型应用场景就是http服务器中的session,这个session是保存用户在服务器端的数据的,这个数据类型可能是字符串,数值,或者用户自定义的对象,在这种情况下无法定义一个variant,这时候用any是合适的。

tf.variant的实现

tf.variant的实现思路

tf.variant的实现思路实际上就是any的实现思路,any对象内部有一个内部基类的unique_ptr,这个基类有一个带模版参数的派生类。

这个派生模版类是泛型的,用它代表所有类型。

当给any对象赋值的时候,我们就创建这个派生类对象,这个对象保存了实际的类型。

在后面取值的时候我们就可以将这个基类向下转型为某个具体的派生类对象了,这时需要判断当前保存的对象类型和传入的类型是否一致,不一致则返回nullptr。

any

tf.variant的源码实现

1.内部定义一个抽象类和一个派生模版类

2.定义一个成员变量,内部接口类的指针–std::unique_ptr< ValueInterface > value_;

3.赋值时创建派生类对象

赋值的时候对赋值类型做了限定,必须为非Variant的,并且有拷贝构造函的类型,因为any内部会重新创建这个对象,之后赋值给基类指针value_,同时实现了类型擦除。

4.取值

取值的时候会判断是否初始化以及类型是否匹配,满足条件就转成实际的类型,否则返回nullptr。

这个代码的实现还是比较简单的,就是实现了一个基本的any语义,还有一些自己扩展的部分,比如encode和decode之类的。

总结

tf.variant本质上就是一个any,用来做类型擦除,像c#,java中的object那样代表一个通用类型。

在tensorflow中很多时候是作为一个函数参数,有些这样代码:

目前还没进一步分析tf的代码,我猜测这样做的目的可能是为了统一的接口以及灵活性,接口可以保持通用不变,具体实现将因具体类型不同而各有不同,方便扩展。

variant原理和应用

variant原理和应用

variant语义

variant是一个泛化的、类型安全的union。可以保存类型不同的对象,它的特点是多类型单值。

基本用法

以c++17中的variant为例(boost中的variant和标准库的用法几乎一样),我们定义一个这种的variant:

std::variant<int, double, char> v;

这个variant可以用来存储int, double和char三种类型的数据,所以我们可以这样赋值:

可以看到类型的值可以赋给同一个variant,如果将一个非int, double和char的类型赋值给v的话则在会出现一个编译期错误,所以variant是类型安全的,variant只允许将定义时指定的那些类型赋值给它。注意,重新赋值的时候之前的对象会自动析构掉。

接下来看如何取值:

通过std::get(v)就可以获取对应的值了,不过取值的时候需要知道当前的variant具体是什么类型,如果类型不对则会抛异常。

这种需要传具体类型的访问variant的方法有局限性,有时候我们不能确切知道当前的具体类型,这种情况下该如何取值呢?
以boost库提供的访问vairant方法为例:

这种访问方法需要定义一个函数对象对每个类型的访问都写一个重载函数,如果找不到对应的重载函数则会报一个编译期错误。

这种方法更常用,因为它不需要知道具体的类型。

还可以用C++11写一个支持lambda的visitor,避免写一个函数对象,用起来可以更简单。

实现原理

虽然很多库自己实现了variant,但其实现原理是类似的,实现variant主要分为下面几步:

1.variant内部定义一个足够大的缓冲区。

足够大的意思是这个缓冲区可以用来创建variant定义时的那些类型,具体多大呢,需要借助模版元方法遍历所有的类型找出其中size最大的那个类型,找到这个最大的size之后我们就可以定义一个栈上的char数组了,这里还需要考虑内存对齐的问题。

2.通过placement new创建某一个类型的对象。

这个对象就在内部的栈上缓冲区中创建的,所有的类型共用这块缓冲区,和union类似。具体创建对象的时候用的是placement new,一个是出于性能考虑,一个出于便于回收之前的对象考虑,因为重新赋值的时候需要将之前的对象析构掉。

3.variant赋值。

赋值时需要保存当前类型的type_index, 便于后面取值的时候判断需要取值的类型和当前类型是否一致。

4.variant的析构。

析构的时候要根据type_index遍历所有的类型找到当前的类型然后调用该类型的析构函数去析构。

通过这个思路就可以实现一个基本的variant,如果需要支持更多功能的时候还需要增加一些代码,比如支持嵌套的variant之类,支持lambda访问等等。

具体的实现可以参考我的github上C++11实现的基本功能的variant:https://github.com/qicosmos/cosmos/blob/master/Variant.hpp.

应用场景

我们一般在什么场景下需要用到variant呢?varaint一般用于类型擦除,比如我们需要访问一个某个值,这个值的类型是有限个的,这时可以用variant来表示这个“可能是多个类型的值”,避免针对具体类型去写代码,可以消除强制转换,或者把复杂问题简化,比如一些异构类型没办法抽象泛化的时候,用variant泛化是很方便的。

一个典型的应用场景就是数据库表访问的场景,数据库表字段的类型是有限多个的,我们完全可以用一个variant来表示数据库字段,接着就可以像通用类型那样去访问数据表字段了,而不必关注具体的类型,把一个复杂问题变得很简单了。

如何引入到我们的工程

boost库中已经有variant了,c++17中也引入了,如果我们的编译器还只支持C++11,则可以直接用boost中的variant,boost中的variant是header only的,直接包含头文件既可以,这里涉及到另外一个问题就是如何引入boost库。

boost库是一个广泛使用具有工业强度的c++库,很多C++新标准里的新特性都是来自于boost,使用里面已经有的库,可以避免重复造轮子,节省开发测试时间,而且质量是有保证的。

那么如何引入boost呢,引入boost有两种方式:

1.直接安装

可以查看当前源中有哪个boost库, 然后直接安装:

安装之后直接在工程中引用boost头文件既可以,以variant为例,使用variant时直接include即可:

2.编译安装

从boost.org官网下载一个版本,解压之后在boost目录执行两个命令就可以了

总结

variant作为一个类型安全的union,可以帮助我们做类型擦除,从而可以避免强制转换,便于编写更加泛化通用的代码,化繁为简。

cinatra–一个高效易用的c++ http框架

目录

cinatra简介

cinatra是一个高性能易用的http框架,它是用modern c++(c++17)开发的,它的目标是提供一个快速开发的c++ http框架。它的主要特点如下:

  1. 统一而简单的接口
  2. header-only
  3. 跨平台
  4. 高效
  5. 支持面向切面编程

cinatra目前支持了http1.1/1.0和websocket, 你可以用它轻易地开发一个http服务器,比如常见的数据库访问服务器、文件上传下载服务器、实时消息推送服务器,你也可以基于cinatra开发一个mqtt服务器。

如何使用

编译依赖

cinatra是基于boost.asio开发的,所以需要boost库,同时也需要支持c++17的编译器,依赖项:

  1. boost.asio, boost1.66
  2. c++17编译器(gcc7.2,clang4.0, vs2017 update15.5)

使用

cinatra是header-only的,直接引用头文件既可。

快速示例

示例1:一个简单的hello world

5行代码就可以实现一个简单http服务器了,用户不需要关注多少细节,直接写业务逻辑就行了。

示例2:展示如何取header和query以及错误返回

示例3:面向切面的http服务器

本例中有两个切面,一个校验http请求的切面,一个是日志切面,这个切面用户可以根据需求任意增加。本例会先检查http请求的合法性,如果不合法就会返回bad request,合法就会进入下一个切面,即日志切面,日志切面会打印出一个before表示进入业务逻辑之前的处理,业务逻辑完成之后会打印after表示业务逻辑结束之后的处理。

示例4:文件上传

cinatra目前支持了multipart和octet-stream格式的上传。

multipart文件上传

短短几行代码就可以实现一个http文件上传的服务器了,包含了异常处理和错误处理。

octet-stream文件上传

示例5:文件下载

示例6:websocket

性能测试

测试用例:

ab测试:ab -c100 -n5000 127.0.0.1:8080/

服务器返回一个hello。

在一个8核心16G的云主机上测试,qps在9000-13000之间。

对比测试

通过ab测试和boost.beast做对比,二者qps相当,大概是因为二者都是基于boost.asio开发的的原因。cinatra目前还没做专门的性能优化,还有提升空间。

注意事项

文件上传下载,websocket的业务函数是会多次进入的,因此写业务逻辑的时候需要注意,推荐按照示例中的方式去做。

cinatra目前刚开始在生产环境中使用, 还处于完善阶段,可能还有一些bug,因此不建议现阶段直接用于生产环境,建议先在测试环境下试用。

试用没问题了再在生产环境中使用,试用过程中发现了问题请及时提issue反馈或者邮件联系我。

测试和使用稳定之后cinatra会发布正式版。

roadmap

  1. 支持ssl
  2. 支持断点续传
  3. 支持session和cookie
  4. 接口优化、性能优化

我希望有越来越多的人使用并喜欢cinatra,也希望cinatra在使用过程中越来越完善,变成一个强大易用、快速开发的http框架,欢迎大家积极参与cinatra项目,可以提issue也可以发邮件提建议,也可以提pr,形式不限。

这次重构的cinatra几乎是重写了一遍,代码比之前的少了30%以上,接口统一了,http和业务分离,具备更好的扩展性和可维护性。

联系方式

purecpp@163.com

http://purecpp.org/

https://github.com/qicosmos/cinatra

致谢

感谢社区的“逐雁南飛”和“非常可乐”两位朋友的帮助,你们帮我澄清了一些http业务细节,同时也提出了一些宝贵意见,在此致以衷心的感谢!

一个C++14模板元实现的深度学习神经网络模板类,支持任意层数

构造编译期矩阵以及数据传递代码,headonly
搜遍了github,在模板元这块机器学习还是空白,正好是个填补,我接下来会逐渐丰富这个库(倒是有几个模板元数学运算库,都很简陋)
大量的矩阵运算用模板元进行有几个让人非常惬意的优势,也发觉模板元其实很适合这种编程
(不知道是否唯有C++才有的优势,数学专用语言不算在内,比如m、r这些):
1、永远不用担心数组越界,也不用写检查数组越界的代码
2、矩阵运算不用检查行列是否匹配,行列的要求通过模板函数参数就能限定了
3、快,只有cpper才懂的快
代码在这里 https://github.com/bowdar/DeepLearning

先看使用方法,过程极其简单

模板类的申明,开头是用来迭代整形模板参数的UnpackInts,根据Index取值,没有使用TypeList
代码使用到的矩阵模板类和数学公式就没贴了

模板类的实现

扩展模板库的新方法 – 善用函数的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月《程序员》杂志,转载请注明出处。