C++利用宏实现类成员反射详解
作者:ticking
序
本文我们看下用宏来实现反射,在一些伙伴使用c++版本还不是那么高的情况下但又需要反射的一些技巧,这里使用的代码是iguana里的实现,我对它关于反射的地方提炼一下,稍微改动了下。iguana是比较优秀的序列化库,其中使用反射为基础,性能很好。现在在yalantinglibs中也可以找到。
当然使用的时候可以直接使用iguana,我这里解释下其中的相关原理。
如何使用
以如下Person
这个结构体为例
struct Person{ int a; float b; }; REFLECTION(Person, a, b)
这里结构体就是普通的结构体,不过需要用户做的是,需要定义REFLECTION
宏,其中第一个参数是类(结构体)名,然后是各个成员名。
然后其实就可以使用反射了:
using Members = decltype(iguana_reflect_members(std::declval<Person>())); std::cout << Members::value() << std::endl; // count auto membersPtr = Members::apply_impl(); // ptr(tuple) Person p{}; p.*std::get<0>(membersPtr) = 34; p.*std::get<1>(membersPtr) = 4.1f; std::cout << p.a << std::endl; // 34 std::cout << p.b << std::endl; // 4.1
REFLECTION
中会生成一个iguana_reflect_member
s函数,该函数简单展示下:
auto iguana_reflect_members(STRUCT_NAME const &) { struct reflect_members { // ...(略) }; return reflect_members{}; }
可以看到iguana_reflect_members
函数内部定义了一个用来提供成员反射信息的b结构体,然后构造并返回。
继续返回到使用示例那里,第一句通过decltype
和declval
搭档拿到了iguana_reflect_members
返回值类型。第二句我们先打印出来Person
这个结构体的成员个数。然后再使用Members::apply_impl
函数获取到Person
的成员指针。这里使用成员指针就可以对其成员进行访问了。返回值的类型是tuple
,我们使用std::get
来对tuple
进行遍历访问。
如何实现
那么如何实现获取到成员的个数,及存储成员指针这些呢,我们去揭开REFLECTION
的真面目;
#define REFLECTION(STRUCT_NAME, ...) \ MAKE_META_DATA_IMPL(STRUCT_NAME, GET_ARG_COUNT(__VA_ARGS__), __VA_ARGS__)
看上去很简单,调用MAKE_META_DATA_IMPL
的宏,MAKE_META_DATA_IMPL
需要STRUCT_NAME
,count以及所有的剩余参数,也就是类的各个成员。可以看到GET_ARG_COUNT
可以获取到成员的个数。
成员个数
那我们先去看下GET_ARG_COUNT
的实现:
#define MARCO_EXPAND(...) __VA_ARGS__ #define GET_ARG_COUNT_INNER(...) MARCO_EXPAND(ARG_N(__VA_ARGS__)) #define GET_ARG_COUNT(...) GET_ARG_COUNT_INNER(__VA_ARGS__, RSEQ_N())
GET_ARG_COUNT
调用GET_ARG_COUNT_INNER
,将成员和RSEQ_N
拼接起来,传递给ARG_N这个宏作为参数调用,那就要看下ARG_N和RSEQ_N的声明:
#define ARG_N(_1, _2, _3, _4, _5, _6, _7, _8, \ _9, _10, _11, _12, _13, _14, _15, \ _16, _17, _18, _19, _20, _21, _22, \ _23, _24, _25, _26, _27, _28, _29, \ _30, _31, _32, N, ...) \ N #define RSEQ_N() \ 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, \ 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, \ 5, 4, 3, 2, 1, 0
ARG_N
可以看到接收32个参数及N,然后就能表示N。RSEQ_N()
仅仅就是32~0的数字序列,那么将成员(假设3个成员)和RSEQ_N()
组合起来就是类似这样
member1, member2, member3, 32, 31, 30, ... 0
然后传递给ARG_N
时,member1对应_1,member2对应_2,member3对应_3,32对应_4,... 这样计算参数个数就是3(3个成员)+ 33(32~0),那么进一步这里N就是第33个元素,再进一步可以这样理解:如果没有成员,仅仅RSEQ_N()
传递进来时,一一匹配到0正好对应N,那么当前边加了3个成员,那么就是将RSEQ_N()
后移3个元素,那就是N正好对应3,也就是成员的个数。
成员指针
再次回到我们实现的最开始部分:
#define REFLECTION(STRUCT_NAME, ...) \ MAKE_META_DATA_IMPL(STRUCT_NAME, GET_ARG_COUNT(__VA_ARGS__), __VA_ARGS__)
GET_ARG_COUNT
这个我们已经明白了,那么我们继续去看下MAKE_META_DATA_IMPL
宏做了什么:
#define MAKE_META_DATA_IMPL(STRUCT_NAME, N, ...) \ [[maybe_unused]] inline static auto \ iguana_reflect_members(STRUCT_NAME const &) { \ struct reflect_members { \ constexpr decltype(auto) static apply_impl(){ \ return std::make_tuple( \ MAKE_ARG_LIST(N, &STRUCT_NAME::FIELD, \ __VA_ARGS__)); \ } \ using size_type = \ std::integral_constant<size_t, N>; \ constexpr static size_t value() { \ return size_type::value; \ } \ }; \ return reflect_members{}; \ }
MAKE_META_DATA_IMPL这个宏就是定义了iguana_reflect_members(STRUCT_NAME const &)
这样一个函数,大致的结构我们前边也说过,值得注意的有两个点:
- 因为参数会根据传入的类名各不一样,所以不用担心函数签名重复的问题;
- 定义了函数内部结构体,返回了内部结构体对象,但是如我们最开始使用那样,仅仅通过decval拿到这个内部结构体的类型,而不会真正调用
iguana_reflect_members
函数。
继续看reflect_members
结构体,从简单的看起,value
函数就是返回刚刚传进来的N,这里size_type
就是一个值为N的结构体,正好也是返回size_type::value
, 所以就是N。
apply_impl
函数里边稍微有点复杂,因为成员的指针类型各不一样,所以使用tuple来存放,内部就是对MAKE_ARG_LIST
的调用,那我们也再跳转到实现瞧瞧:
#define MACRO_CONCAT(A, B) MACRO_CONCAT1(A, B) #define MACRO_CONCAT1(A, B) A##_##B #define MAKE_ARG_LIST(N, op, arg, ...) \ MACRO_CONCAT(MAKE_ARG_LIST, N)(op, arg, __VA_ARGS__)
由于宏的一些特性,我们不得不使用MACRO_CONCAT1
及MACRO_CONCAT
对宏与一些字符进行拼接。这里是把MAKE_ARG_LIST和下划线以及N进行拼接,那么MAKE_ARG_LIST
实际上调用的是MAKE_ARG_LIST_N
,但是这里的N是实际的成员个数,还是假定是3个成员,那么调用就是这样的MAKE_ARG_LIST_3(op, arg, __VA_ARGS__)
,同时这里还用arg来拆出来第一个元素,类似于我们解参数包方式。
那么我们还需要再次去看MAKE_ARG_LIST_3
的实现:
#define MAKE_ARG_LIST_1(op, arg, ...) op(arg) #define MAKE_ARG_LIST_2(op, arg, ...) \ op(arg), MARCO_EXPAND(MAKE_ARG_LIST_1(op, __VA_ARGS__)) #define MAKE_ARG_LIST_3(op, arg, ...) \ op(arg), MARCO_EXPAND(MAKE_ARG_LIST_2(op, __VA_ARGS__)) #define MAKE_ARG_LIST_4(op, arg, ...) \ op(arg), MARCO_EXPAND(MAKE_ARG_LIST_3(op, __VA_ARGS__)) //...(略) #define MAKE_ARG_LIST_32(op, arg, ...) \ op(arg), MARCO_EXPAND(MAKE_ARG_LIST_31(op, __VA_ARGS__))
因为MAKE_ARG_LIST_N
和成员个数有关,这里也还是定义了32个宏,中间我们省略了很多,实现很简单,先看MAKE_ARG_LIST_1
,就是对op的调用,再看MAKE_ARG_LIST_2
首先对第一个参数进行op调用,剩下的参数去调用MAKE_ARG_LIST_1
,然后使用逗号拼接。以此类推,如果是32的话,就是对32个参数分别op调用。
我们继续跳回到成员指针获取的那里:
#define FIELD(t) t struct reflect_members { constexpr decltype(auto) static apply_impl() { return std::make_tuple( MAKE_ARG_LIST(N, &STRUCT_NAME::FIELD,__VA_ARGS__) ); } };
我们明白了MAKE_ARG_LIST
的含义,就是分别对各个参数进行op操作,这里op正好对应于&STRUCT_NAME::FIELD
, FIELD
就是一个封装了一个括号方便调用,那也就是&STRUCT_NAME::
各个成员,这也就是成员的指针。
最终推导展示
有了上边的讲解,我们使用clion可以看到最开始Person
的REFLECTION(Person, a, b)
表示的是啥:
增加成员类型
尽管可以通过指针获取到各个成员的类型,但是为了使用方便,我们在reflect_members
中增加一个各个成员的类型,我们使用类型列表来存放:
template<typename... Types> struct TypeList {};
这样reflect_members
中成员类型列表可能实现就是这样:
struct reflect_members { using member_types = TypeList< MAKE_ARG_LIST(N, decltype, MAKE_ARG_LIST(N, STRUCT_NAME::FIELD, __VA_ARGS__)) >; };
可以看到,TypeList中使用两个MAKE_ARG_LIST
嵌套实现,首先对每个成员参数STRUCT_NAME::FIELD
操作,然后在对操作后的成员参数进行decltype操作,以上面Person为例可以看到最终member_types
是这样的:
using member_types = TypeList<decltype(Person::a), decltype(Person::b)>;
然后我们如何使用TypeList,需要配套一些操作方法,我这里目前只实现了根据顺序来获取成员的类型,类似这样:
using MemberTypes = Members::member_types; TypeByIndex<0, MemberTypes>::type a1 = 12; // int TypeByIndex<1, MemberTypes>::type b1 = 12.87; // float
我们也简单看下TypeByIndex
如何实现:
template<int Index, typename TL> struct TypeByIndex { using type = typename TypeByIndex<Index - 1, typename ListPop<TL>::type>::type; }; template<typename TypeList> struct TypeByIndex<0, TypeList> { using type = typename ListHead<TypeList>::type; };
TypeByIndex
模板元函数实现如上,针对于Index为0进行特化,那就是说只需要获取TypeList中第一个类型即可,也就是这里的ListHead
。否则就走主模板,主模板是一个递归的,将Index减1,TypeList给pop出第一个元素,也即ListPop
操作,继续调用TypeByIndex
,直到Index为0,正好对应于相对应的类型。
也看下ListHead
和ListPop
的实现:
template<typename TL> struct ListHead; template<typename Head, typename... Args> struct ListHead<TypeList<Head, Args...>> { using type = Head; }; template<typename Tp> struct ListPop { using type = TypeList<>; }; template<typename Head, typename... Args> struct ListPop<TypeList<Head, Args...>> { using type = TypeList<Args...>; };
- 先看
ListHead
,主模板仅仅是一个声明,特化版本特化出来TypeList<Head, Args...>
直接获取到Head。 - 再来看
ListPop
,主模板认为是一个空的TypeList,特化模板则是特化出来TypeList<Head, Args...>
形式,那样正好把第一个元素和后边元素分开,进一步拿到后边类型重新组装成新的TypeList。
总结
这里我们使用宏来实现了结构体(或类)成员的反射,包括成员的个数,成员的指针,成员的类型。有了这些我们就可以做一些基本的操作了,比如说一些序列化结构体等等。
我们还展示了TypeList及相关的简单操作。当然你如果需要的话,也可以将TypeList操作丰富起来。
以上就是C++利用宏实现类成员反射详解的详细内容,更多关于C++反射的资料请关注脚本之家其它相关文章!