C++利用宏实现类成员反射详解

 更新时间:2024年01月10日 11:36:56   作者:ticking  
这篇文章主要为大家详细介绍了C++如何利用宏实现类成员反射,文中的示例代码讲解详细,具有一定的学习价值,有兴趣的小伙伴可以了解一下

脚本之家 / 编程助手:解决程序员“几乎”所有问题!
脚本之家官方知识库 → 点击立即使用

本文我们看下用宏来实现反射,在一些伙伴使用c++版本还不是那么高的情况下但又需要反射的一些技巧,这里使用的代码是iguana里的实现,我对它关于反射的地方提炼一下,稍微改动了下。iguana是比较优秀的序列化库,其中使用反射为基础,性能很好。现在在yalantinglibs中也可以找到。

当然使用的时候可以直接使用iguana,我这里解释下其中的相关原理。

如何使用

以如下Person这个结构体为例

1
2
3
4
5
struct Person{
    int a;
    float b;
};
REFLECTION(Person, a, b)

这里结构体就是普通的结构体,不过需要用户做的是,需要定义REFLECTION宏,其中第一个参数是类(结构体)名,然后是各个成员名。

然后其实就可以使用反射了:

1
2
3
4
5
6
7
8
9
10
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函数,该函数简单展示下:

1
2
3
4
5
6
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的真面目;

1
2
#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的实现:

1
2
3
4
#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的声明:

1
2
3
4
5
6
7
8
9
10
11
#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,也就是成员的个数。

成员指针

再次回到我们实现的最开始部分:

1
2
#define REFLECTION(STRUCT_NAME, ...)                                    \
 MAKE_META_DATA_IMPL(STRUCT_NAME, GET_ARG_COUNT(__VA_ARGS__), __VA_ARGS__)

GET_ARG_COUNT这个我们已经明白了,那么我们继续去看下MAKE_META_DATA_IMPL宏做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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的调用,那我们也再跳转到实现瞧瞧:

1
2
3
4
5
#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的实现:

1
2
3
4
5
6
7
8
9
10
11
12
#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调用。

我们继续跳回到成员指针获取的那里:

1
2
3
4
5
6
7
8
9
10
#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中增加一个各个成员的类型,我们使用类型列表来存放:

1
2
template<typename... Types>
struct TypeList {};

这样reflect_members中成员类型列表可能实现就是这样:

1
2
3
4
5
6
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是这样的:

1
using member_types = TypeList<decltype(Person::a), decltype(Person::b)>;

然后我们如何使用TypeList,需要配套一些操作方法,我这里目前只实现了根据顺序来获取成员的类型,类似这样:

1
2
3
4
using MemberTypes = Members::member_types;
    
TypeByIndex<0, MemberTypes>::type a1 = 12;    // int
TypeByIndex<1, MemberTypes>::type b1 = 12.87; // float

我们也简单看下TypeByIndex如何实现:

1
2
3
4
5
6
7
8
9
10
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的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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++反射的资料请关注脚本之家其它相关文章!

蓄力AI

微信公众号搜索 “ 脚本之家 ” ,选择关注

程序猿的那些事、送书等活动等着你

原文链接:https://juejin.cn/post/7321943946308878376

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 reterry123@163.com 进行投诉反馈,一经查实,立即处理!

相关文章

  • 深入剖析Android中init进程实现的C语言源码

    深入剖析Android中init进程实现的C语言源码

    这篇文章主要介绍了Android中init进程实现的C语言源码,init属性服务在安卓中属于系统的底层Linux服务,需要的朋友可以参考下
    2015-07-07
  • C语言中cJSON的使用

    C语言中cJSON的使用

    JSON是一种轻量级的数据交换格式,常用于在网络之间传输数据,本文主要介绍了C语言中cJSON的使用,具有一定的参考价值,感兴趣的可以了解一下
    2024-04-04
  • C++中md5 算法实现代码

    C++中md5 算法实现代码

    在网上找了份c++ MD5的代码,就简单保存一下,需要的朋友可以参考下
    2017-07-07
  • C++模板的特化超详细精讲

    C++模板的特化超详细精讲

    最近我学习了C++中的模板相关知识,模板是泛型编程的基础,十分重要。所以特意整理出来一篇文章供我们一起复习和学习
    2022-08-08
  • C语言实现合式公式的判断示例

    C语言实现合式公式的判断示例

    这篇文章主要介绍了C语言实现合式公式的判断示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-04-04
  • c++栈内存和堆内存的基本使用小结

    c++栈内存和堆内存的基本使用小结

    本文主要介绍了c++栈内存和堆内存的基本使用小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-07-07
  • operator new在C++中的各种写法总结

    operator new在C++中的各种写法总结

    这篇文章并不是一个综合的手册,而是一个C++中各种内存分配方法的概述。它面向已经很熟悉C++语言的读者
    2013-09-09
  • C语言实现高精度加法的示例代码

    C语言实现高精度加法的示例代码

    高精度的本质是将数字以字符串的形式读入,然后将每一位分别存放入int数组中,通过模拟每一位的运算过程,来实现最终的运算效果,下面我们就来看看如何通过C语言实现高精度加法吧
    2023-11-11
  • C++ win系统如何用MinGW编译Boost库

    C++ win系统如何用MinGW编译Boost库

    这篇文章主要介绍了C++ win系统如何用MinGW编译Boost库问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-12-12
  • C++数组的定义详情

    C++数组的定义详情

    这篇文章主要介绍了C++数组的定义详情,上一篇文章我们学习了类型,接下俩我们九在类型的基础上展开本篇内容数组的常用方法以及C++标准库提供的一些关于数组的容器,需要的朋友可以参考一下,希望对你有所帮助
    2021-12-12

最新评论