本文我们看下用宏来实现反射,在一些伙伴使用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()));
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; \
constexpr static size_t value() { \
return size_type::value; \
} \
}; \
return reflect_members{}; \
}
MAKE_META_DATA_IMPL这个宏就是定义了iguana_reflect_members(STRUCT_NAME const &)
这样一个函数,大致的结构我们前边也说过,值得注意的有两个点:
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
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;
然后我们如何使用TypeList,需要配套一些操作方法,我这里目前只实现了根据顺序来获取成员的类型,类似这样:
using MemberTypes = Members::member_types;
TypeByIndex<0, MemberTypes>::type a1 = 12; // int
TypeByIndex<1, MemberTypes>::type b1 = 12.87; // float
我们也简单看下TypeByIndex
如何实现:
template
struct TypeByIndex {
using type = typename TypeByIndex::type>::type;
};
template
struct TypeByIndex<0, TypeList> {
using type = typename ListHead::type;
};
TypeByIndex
模板元函数实现如上,针对于Index为0进行特化,那就是说只需要获取TypeList中第一个类型即可,也就是这里的ListHead
。否则就走主模板,主模板是一个递归的,将Index减1,TypeList给pop出第一个元素,也即ListPop
操作,继续调用TypeByIndex
,直到Index为0,正好对应于相对应的类型。
我们也看下ListHead
和ListPop
的实现:
template
struct ListHead;
template
struct ListHead> {
using type = Head;
};
template
struct ListPop {
using type = TypeList<>;
};
template
struct ListPop> {
using type = TypeList;
};
ListHead
,主模板仅仅是一个声明,特化版本特化出来TypeList
直接获取到Head。ListPop
,主模板认为是一个空的TypeList,特化模板则是特化出来TypeList
形式,那样正好把第一个元素和后边元素分开,进一步拿到后边类型重新组装成新的TypeList。这里我们使用宏来实现了结构体(或类)成员的反射,包括成员的个数,成员的指针,成员的类型。有了这些我们就可以做一些基本的操作了,比如说一些序列化结构体等等。
同时我们还展示了TypeList及相关的简单操作。当然你如果需要的话,也可以将TypeList操作丰富起来。