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

本文我们看下用宏来实现反射,在一些伙伴使用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; // countauto 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) tstruct 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++模板 第二版》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/604901.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android TCP、UDP区别

目录 TCP、UDP区别 连接性 可靠性 流量控制和拥塞控制 应用场景 小结 Android 中的TCP、UDP TCP&#xff08;传输控制协议&#xff09; UDP&#xff08;用户数据报协议&#xff09; 小结 TCP、UDP区别 TCP&#xff08;传输控制协议&#xff09;和UDP&#xff08;用户…

Java开发HttpSession详解

Java开发HttpSession详解 大家好&#xff0c;我是免费搭建查券返利机器人赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天&#xff0c;让我们一同深入探讨Java中的HttpSession&#xff0c;了解其在Web开发中的…

win10下vscode+cmake编译C代码操作详解

0 工具准备 1.Visual Studio Code 1.85.1 2.cmake 3.24.01 前言 当我们只有一个.c文件时直接使用vscodeCode Runner插件即可完成编译&#xff0c;如果我们的工程很复杂包含多个.c文件时建议使用cmake来生成对应的make&#xff0c;指导编译器完成编译&#xff0c;否则会提示各…

【二叉树】二叉树根节点到叶子节点的所有路径和

题目&#xff0c;来自牛客网 法1&#xff1a;使用全局变量 public class Solution {public int res 0;public int sumNumbers (TreeNode root) {if (root null) {return 0;}dfs(root, 0);return res;}public void dfs(TreeNode root, int preSum) {if (root.left null &am…

强化学习的数学原理学习笔记 - 基于模型(Model-based)

文章目录 概览&#xff1a;RL方法分类基于模型&#xff08;Model-Based&#xff09;值迭代&#xff08;Value Iteration&#xff09;&#x1f7e6;策略迭代&#xff08;Policy Iteration&#xff09;&#x1f7e1;截断策略迭代&#xff08;Truncated Policy Iteration&#xff…

YOLOv5改进 | 损失函数篇 | EIoU、SIoU、WIoU、DIoU、FocusIoU等二十余种损失函数

一、本文介绍 这篇文章介绍了YOLOv5的重大改进,特别是在损失函数方面的创新。它不仅包括了多种IoU损失函数的改进和变体,如SIoU、WIoU、GIoU、DIoU、EIOU、CIoU,还融合了“Focus”思想,创造了一系列新的损失函数。这些组合形式的损失函数超过了二十余种,每种都针对特定的…

K8S--安装MySQL8(单机)

原文网址&#xff1a;K8S--安装MySQL8&#xff08;单机&#xff09;-CSDN博客 简介 本文介绍K8S部署MySQL8&#xff08;单机&#xff09;的方法。 本文的目标 1.通过PV和PVC&#xff08;hostPath方式&#xff09;存储MySQL的数据 2.通过Deployment、Service部署MySQL8&…

Java异常机制:从混乱到控制的错误管理艺术

&#x1f451;专栏内容&#xff1a;Java⛪个人主页&#xff1a;子夜的星的主页&#x1f495;座右铭&#xff1a;前路未远&#xff0c;步履不停 目录 一、异常的体系结构1、异常的体系结构2、异常的分类 二、异常的处理1、异常的抛出2、异常的捕获2.1、异常声明throws2.2、try-c…

leaflet:加载本地shp文件,并在地图上显示出来 (138)

第138个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet中加载本地shp文件,利用shapefile读取shp数据,转换为json,利用L.geoJSON()在地图上显示图形。 直接复制下面的 vue+openlayers源代码,操作2分钟即可运行实现效果 文章目录 示例效果安装加载shapefile.j…

Docker使用扩展

日升时奋斗&#xff0c;日落时自省 目录 1、容器 1.1、容器的生命周期 1.1.1、容器OOM 1.1.2、容器异常退出 1.1.3、容器暂停 1.2、容器命令 1.2.1、创建容器 1.2.2、启动容器 1.2.3、容器日志 1.2.4、容器交互 1.2.5、容器停止 1.2.6、扩展 1.3、综合演示 2、存…

ChatGPT | 模型架构 | 应用 | 思考

介绍 ChatGPT 3.5 是 OpenAI 推出的语言模型的一个版本&#xff0c;是 GPT&#xff08;生成式预训练模型&#xff09;系列的一部分。在自然语言理解和生成方面具有强大的能力&#xff0c;可以应用于问答系统、文本生成、翻译和对话系统等多个领域。 模型架构 GPT-3.5&#x…

【代码】Keras3.0:实现残差连接

简介 残差连接是一种非常重要的网络结构创新&#xff0c;最早被广泛应用于ResNet&#xff08;Residual Neural Network&#xff09;模型中&#xff0c;由何凯明等人在2015年的论文"Deep Residual Learning for Image Recognition"中提出。 核心思想 通过引入“short…

如何查找文献及相关代码

参考文献 1.文献调研之如何查找文献及源码_在哪个网站能下载有代码的文献-CSDN博客 2.如何寻找论文及其相关代码&#xff1f;_google学术中的论文代码怎么找-CSDN博客 3.找论文代码_researchcode-CSDN博客 4. 如何最快速找到自己想要的优质的论文以及代码?一个超强大的网站…

Vue3-42-组件-给组件指定名称 + defineOptions 函数的简单使用

问题说明 当我们在 使用 <script setup> 组合式 API的时候&#xff0c;发现我们并没有给组件指定它的名称。 此时它实际上使用的是 文件的名称 作为组件的名称。 例如 &#xff1a; 组件文件是 【componentABC.vue】 那么这个组件的默认名称就是 【componentABC】.现在我…

Linux stm32串口下载程序

一、工具 使用stm32flash进行串口下载 二、stm32flash安装 sudo apt-get install stm32flash 三、查看串口设备名称 先拔掉串口运行下面指令&#xff0c;获得所有设备名称,插上串口再运行一次&#xff0c;新增的就是串口设备名称&#xff0c;记住串口设备名称&#xff0c;以…

【Hadoop】说下HDFS读文件和写文件的底层原理?

文件读取文件的写入 文件读取 客户端调用 FileSystem 对象的 open&#xff08;&#xff09;函数&#xff0c;打开想要读取的文件。其中FileSystem 是 DistributeFileSystem 的一个实例&#xff1b;DistributedFileSystem 通过使用 RPC&#xff08;远程过程调用&#xff09; 访N…

Linux rpm命令教程:如何使用rpm命令进行软件包管理(附实例详解和注意事项)

Linux rpm命令介绍 rpm命令&#xff0c;全称为Red Hat Package Manager&#xff0c;是用于管理Linux各项套件的程序。它最初是由Red Hat Linux发行版专门用来管理Linux各项套件的程序&#xff0c;由于它遵循GPL规则且功能强大方便&#xff0c;因而广受欢迎&#xff0c;逐渐受到…

简单讲述网络安全的概念、类型和重要性

什么是网络安全&#xff1f; 网络安全是指用于防止网络攻击或减轻其影响的任何技术、措施或做法。网络安全旨在保护个人和组织的系统、应用程序、计算设备、敏感数据和金融资产&#xff0c;使其免受简单而不堪其绕的计算机病毒、复杂而代价高昂的勒索软件攻击&#xff0c;以及介…

Unity 了解Input Manage下默认的输入轴

在Unity菜单Edit->Project Settings->Input Manager->Axes下有一些默认的输入轴&#xff0c;如 这些输入轴代表不同类型的输入&#xff0c;其中&#xff1a; Horizontal&#xff1a;水平移动输入轴。通常与键盘的左右箭头键、A和D键、游戏手柄的左摇杆水平轴等相关联…

【AI视野·今日CV 计算机视觉论文速览 第282期】Wed, 3 Jan 2024

AI视野今日CS.CV 计算机视觉论文速览 Wed, 3 Jan 2024 Totally 70 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computer Vision Papers Street Gaussians for Modeling Dynamic Urban Scenes Authors Yunzhi Yan, Haotong Lin, Chenxu Zhou, Weijie Wang, Haiya…