一种更通用的编译期反射

magic_get编译期反射的局限性

magic_get可以实现编译期获取pod类型,是以一种“无痕”的方式实现的,即无需宏、特殊标记、专门工具。看起来确实很精妙,不过也存在一些局限性,比如只能支持pod类型,不能获取反射类型的字段名,也不支持遍历访问对象字段。这些局限性导致magic_get无法在更广泛的环境下应用。

一种更通用的编译期反射方法

基本的反射功能应该包括根据索引获取字段,根据索引获取字段名,遍历对象的所有字段,支持所有类型的对象。
一种更通用的编译期反射应该支持下面这些。

这种编译期反射有几个优点:使用简单,通用,接口完备,支持所有类型的对象而不仅仅是pod类型,非侵入式。

存在的不足之处在于需要定义一个宏,这个非侵入式的宏是用来获取对象的元数据的,是不可少的。magic_get之所以不需要定义宏,是因为利用了pod类型内存连续的特殊性,可以直接转换为内存连续的tuple,这个tuple提供了元数据,不过这个元数据是有缺陷的,即只有字段值而没有其他信息。而定义宏的方式则提供了丰富而完整的元数据信息,会更通用和方便。

由于这种编译期反射方式把对象元数据和元数据的操作分离了,所以用户可以基于这个通用的编译期反射很自由地做自己感兴趣的事情。

编译期反射可以用来做什么

编译期反射非常适合用来做ORM引擎和序列化/反序列化引擎,以ORM为例,我们可以基于反射来做多种数据库的ORM,比如sqlite, mysql, postgresql,oracle,sqlserver等数据库。有了ORM之后使用起来会非常方便,ORM给用户提供简单通用的接口,把数据库差异、对象和实体相互转换等繁琐的细节都屏蔽了。以基于编译期反射实现sqlite的ORM为例:

从上面的例子可以看到,基于编译期反射实现的ORM接口非常简单、通用和强大,你几乎可以用这几个接口做任何事。即使对于其他数据库,接口仍然保持不变,这将会极大地提高数据库开发效率和降低数据库开发的难度,这就是编译期反射的威力!

关于编译期反射的实现原理和ORM的实现原理敬请关注即将开始的purecpp社区第一期技术公开课

purecpp社区第一期技术公开课(报名截止到1.5号)

背景

modern C++国外已经用得如火如荼了,而国内大多还处于观望阶段,modern C++应该被更广泛地应用。我作为modern C++的倡导者和实践者,希望通过一些技术培训,将最新的C++特性和新技术思想介绍给C++爱好者,让大家不仅仅能深入理解新特性,还能体会到新特性是如何解决一些难题,以及最佳实践。真心希望modern C++能走进更多的企业,让更多的C++开发者享受新标准带来的好处,本次技术公开课算是推广modern C++的一种尝试,看看实际效果如何,我会根据实际效果来决定未来是否继续开课。

课程介绍

本次技术公开课的主题是modern c++实现编译期反射。反射是一种根据元数据来获取类内部信息的机制,通过元数据就可以获取对象的字段和方法等信息。C#和java的反射机制都是通过获取对象的元数据来实现的。反射可以用于依赖注入、ORM对象-实体映射、序列化和反序列化等与对象本身信息密切相关的领域。比如,java的Spring框架,其依赖注入的基础是建立在反射的基础之上的,可以根据元数据获取类型的信息并动态创建对象。ORM对象-实体之间的映射也是通过反射实现的。java和c#都是基于中间运行时的语言,中间运行时提供了反射机制,所以反射对于运行时语言来说很容易,但是对于没有中间运行时的语言,要想实现反射是很困难的。

幸运的是使用modern c++的新特性和一些模版元编程技巧可以实现一种通用的非侵入式的编译期反射

课程面向的用户是C++中高级开发者,总共分为三次课程:

  1. C++11/14实现编译期反射的技术基础

  2. C++11/14实现编译期反射的实现

  3. C++11/14实现编译期反射的应用

第一次课主要内容

第一次课的主题是:C++11/14实现编译期反射的技术基础

这次课程是为后续课程做铺垫,介绍实现编译期反射所需要用到的一些新特性和一些元编程技巧。

主要内容为:

C++11特性

  • 完美转发
  • tuple
  • type_traits
  • 可变模版参数

C++14特性

  • constexpr
  • void_t
  • std::index_sequence
  • auto function
  • auto lambda
  • decltype(auto)

其他

  • 宏元
  • SFINAE

第二次课主要内容

主题:C++11/14实现编译期反射的实现
主要内容待定

第三次课主要内容

主题:C++11/14实现编译期反射的应用
主要内容待定

如何报名

将报名信息发到我的邮箱qicosmos@163.com, 报名信息包括:姓名,邮箱,电话,所在公司。

公开课以网络直播或者视频方式进行,报名费用为400,如果你听课之后发现完全听不懂,退还报名费。

具体的开课时间(在某一个周末或者晚上)我会通过邮件告知报名用户。

如果有企业用户需要modern c++技术咨询服务也可以联系我。

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月《程序员》杂志,转载请注明出处。

Copy Protected by Chetan's WP-Copyprotect.