C++用宏实现类成员反射

发布时间:2024年01月08日

本文我们看下用宏来实现反射,在一些伙伴使用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_members函数,该函数简单展示下:

auto iguana_reflect_members(STRUCT_NAME const &) {                                                  
    struct reflect_members {                                                  
        // ...(略)
    };                               
    return reflect_members{}; 
}

可以看到iguana_reflect_members函数内部定义了一个用来提供成员反射信息的b结构体,然后构造并返回。

继续返回到使用示例那里,第一句通过decltypedeclval搭档拿到了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_CONCAT1MACRO_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可以看到最开始PersonREFLECTION(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,正好对应于相对应的类型。

我们也看下ListHeadListPop的实现:

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操作丰富起来。

ref

  • https://github.com/qicosmos/iguana
  • 《C++模板 第二版》
文章来源:https://blog.csdn.net/leapmotion/article/details/135448181
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。