在C++上实现反射用法

embedded/2024/11/27 10:02:40/

0. 简介

最近看很多端到端的工作,发现大多数都是基于mmdet3d来做的,而这个里面用的比较多的形式就是反射机制,这样其实可以比较好的通过类似plugin的形式完成模型模块的插入。当然我们这里不是来分析python的反射机制的。我们这篇文章主要来介绍C++上实现反射。

1. 反射的用途

一般来说就是序列化反序列化啦。比如说你想通过网络传递一个实例,或者把它保存到文件里,以后再取出来放到程序里,这就需要反射

反射其实还能细分为静态反射和动态反射

  1. 静态反射,就是在编译期生成反射信息
  2. 动态反射,就是在运行时生成反射信息
  3. 动态反射,显然需要一套强大的运行时和动态类型系统,也是显然的很复杂

在来,还有侵入式非侵入式之分。非侵入式的反射允许不对源码进行修改就能实现反射;侵入式呢就得对源码动动手脚了

第一种:实现思路,是在源码里加入大量的反射信息,手动注册反射。 这种库的代表是rttr

第二种:实现思路是通过parser解析源码,自动生成反射信息。
这种库的代表是QT,UE的反射系统

第三种: 用大量的编译期模板生成元信息,然后构建一套巨抽象的运行时,比如Ubpa/UDRefl

第四种: 利用调试器的运行时信息来生成反射代码,这种想法并非无稽之谈,思考下,lldb,gdb明显能在运行时获取字段,内容,类型

第五种: 绑架编译器! clang提供了插件功能。事实上也有大佬做了,这些都有比较详细的例子。

2. 源码添加,手动注册

这种用的是比较多的,一般的是自定义一个反射类,然后用模板来实现一个模板类管理类名和类构造函数的映射关系,并提供构造对象的接口,每个基类需要初始化一个这样的管理对象。

下面我们提供一个对应的 static 模板函数,用来保存和返回对应的管理对象。并使用模板函数和 new 操作符作为每个类的构造函数。 实现一个简单的 helper 模板类提供作为注册的简单封装,并封装宏实现注册。下面是具体代码:

#ifndef __BASE_H__
#define __BASE_H__
#include <string>
#include <map>
#include <iostream>// 使用模板,每个基类单独生成一个 ClassRegister
// 好处是需要反射的类不需要去继承 Object 对象
// ClassRegister 用来管理类名->类构造函数的映射,对外提供根据类名构造对象对函数
template<typename ClassName>
class ClassRegister {public:typedef ClassName* (*Constructor)(void);private:typedef std::map<std::string, Constructor> ClassMap;ClassMap constructor_map_;public:// 添加新类的构造函数void AddConstructor(const std::string class_name, Constructor constructor) {typename ClassMap::iterator it = constructor_map_.find(class_name);if (it != constructor_map_.end()) {std::cout << "error!";return;}constructor_map_[class_name] = constructor;}// 根据类名构造对象ClassName* CreateObject(const std::string class_name) const {typename ClassMap::const_iterator it = constructor_map_.find(class_name);if (it == constructor_map_.end()) {return nullptr;}return (*(it->second))();}
};// 用来保存每个基类的 ClassRegister static 对象,用于全局调用
template <typename ClassName>
ClassRegister<ClassName>& GetRegister() {static ClassRegister<ClassName> class_register;return class_register;
}// 每个类的构造函数,返回对应的base指针
template <typename BaseClassName, typename SubClassName>
BaseClassName* NewObject() {return new SubClassName();
}// 为每个类反射提供一个 helper,构造时就完成反射函数对注册
template<typename BaseClassName>
class ClassRegisterHelper {public:ClassRegisterHelper(const std::string sub_class_name,typename ClassRegister<BaseClassName>::Constructor constructor) {GetRegister<BaseClassName>().AddConstructor(sub_class_name, constructor);}~ClassRegisterHelper(){}
};// 提供反射类的注册宏,使用时仅提供基类类名和派生类类名
#define RegisterClass(base_class_name, sub_class_name) \static ClassRegisterHelper<base_class_name> \sub_class_name##_register_helper( \#sub_class_name, NewObject<base_class_name, sub_class_name>);// 创建对象的宏
#define CreateObject(base_class_name, sub_class_name_as_string) \GetRegister<base_class_name>().CreateObject(sub_class_name_as_string)#endif

下面是使用的示例:

#include <iostream>
#include <memory>
#include <cstring>
#include "base3.h"
using namespace std;class base
{public:base() {}virtual void test() { std::cout << "I'm base!" << std::endl; }virtual ~base() {}
};class A : public base
{public:A() { cout << " A constructor!" << endl; }virtual void test() { std::cout << "I'm A!" <<std::endl; }~A() { cout << " A destructor!" <<endl; }
};// 注册反射类 A
RegisterClass(base, A);class B : public base
{public :B() { cout << " B constructor!" << endl; }virtual void test() { std::cout << "I'm B!"; }~B() { cout << " B destructor!" <<endl; }
};// 注册反射类 B
RegisterClass(base, B);class base2
{public:base2() {}virtual void test() { std::cout << "I'm base2!" << std::endl; }virtual ~base2() {}
};class C : public base2
{public :C() { cout << " C constructor!" << endl; }virtual void test() { std::cout << "I'm C!" << std::endl; }~C(){ cout << " C destructor!" << endl; }
};// 注册反射类 C
RegisterClass(base2, C);int main()
{// 创建的时候提供基类和反射类的字符串类名base* p1 = CreateObject(base, "A");p1->test();delete p1;p1 = CreateObject(base, "B");p1->test();delete p1;base2* p2 = CreateObject(base2, "C");p2->test();delete p2;return 0;
}

3. parser解析源码,自动生成反射信息

要实现自动生成反射信息的功能,我们需要编写一个代码解析器,用于解析源代码并提取出需要的信息。一般来说,代码解析器会将源码转换为一棵抽象语法树(AST),然后对这棵树进行遍历,提取出需要的信息。

要使用Clang来解析源码并自动生成反射信息,可以借助Clang的AST(Abstract Syntax Tree)来实现。以下是一个简单的示例代码,演示如何使用Clang来解析源码并生成反射信息:

#include <iostream>
#include <string>
#include "clang/Tooling/Tooling.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Tooling/Tooling.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/ASTContext.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"using namespace clang;
using namespace clang::tooling;
using namespace clang::ast_matchers;class ReflectionGenerator : public MatchFinder::MatchCallback {
public:virtual void run(const MatchFinder::MatchResult &Result) {if (const CXXRecordDecl *Record = Result.Nodes.getNodeAs<CXXRecordDecl>("class")) {std::string className = Record->getNameAsString();std::cout << "Registering class: " << className << std::endl;// 在这里可以生成反射信息并注册类// 获取类名,并输出到控制台std::cout << "Class name: " << className << std::endl;// 遍历类的字段,并输出到控制台for (const FieldDecl *Field : Record->fields()) {std::string fieldName = Field->getNameAsString();std::cout << "Field name: " << fieldName << std::endl;}}}
};int main(int argc, const char **argv) {CommonOptionsParser OptionsParser(argc, argv);ClangTool Tool(OptionsParser.getCompilations(), OptionsParser.getSourcePathList());ReflectionGenerator Generator;MatchFinder Finder;Finder.addMatcher(cxxRecordDecl().bind("class"), &Generator);return Tool.run(newFrontendActionFactory(&Finder).get());
}

在这个示例代码中,我们通过Clang的AST来解析源码,并使用AST Matchers来匹配C++类的声明。当匹配到一个类声明时,ReflectionGenerator类的run方法会被调用,我们可以在这里生成反射信息并注册类。下面我们对上面代码中主要部分进行详细解释:

  1. ReflectionGenerator类:这是一个继承自MatchFinder::MatchCallback的自定义类,用于处理匹配到的AST节点。在run方法中,根据匹配到的C++类定义,获取类名并输出到控制台,同时遍历类的字段并输出字段名。

  2. main函数

    • 创建CommonOptionsParser对象和ClangTool对象,用于解析命令行参数和运行Clang工具。
    • 创建ReflectionGenerator对象和MatchFinder对象,用于注册匹配规则和处理匹配结果。
    • 通过Tool.run方法运行Clang工具,并传入匹配规则和处理结果的工厂对象。
  3. cxxRecordDecl matcher:使用Finder.addMatcher添加了一个匹配规则,用于匹配C++类的定义。当匹配到符合规则的AST节点时,会调用ReflectionGeneratorrun方法进行处理。

  4. 反射信息生成:在ReflectionGeneratorrun方法中,获取到类名和字段名后,输出到控制台。这里展示了获取类名和字段名的基本操作,您可以根据需求进一步扩展生成反射信息的逻辑。

此外这里我们可以通过attribute((annotate(...))) 来完成相同的操作。__attribute__((annotate(...))) 的意义是为代码中的类、字段、函数等元素添加自定义的元数据信息。这些信息可以用于实现反射、元编程、代码生成等功能。通过注解,我们可以为代码中的各种元素添加描述、标签、类型信息等,使其更具有可读性和可维护性,同时也可以在程序运行时动态地获取这些信息并进行相应的操作。在下面的示例中,我们使用了 __attribute__((annotate("reflect_class", "BarClass")))__attribute__((annotate("reflect_property", "int foo")))__attribute__((annotate("reflect_func", "void setFoo(int)")) 等注解来为类、字段和函数添加反射信息。这些注解可以帮助我们在编译时或运行时识别和操作这些元素,实现更高级的功能。

以下是一个完整的示例代码,其中使用了属性拓展和注解来实现反射功能:

#include <iostream>#define RFL_CLASS(...) __attribute__((annotate("reflect_class", #__VA_ARGS__)))
#define RFL_PROPERTY(...) __attribute__((annotate("reflect_property", #__VA_ARGS__)))
#define RFL_FUNC(...) __attribute__((annotate("reflect_func", #__VA_ARGS__)))class RFL_CLASS("BarClass") Bar {
public:RFL_PROPERTY("int foo") int foo;RFL_FUNC("void setFoo(int)") void setFoo(int value) {foo = value;}RFL_FUNC("int getFoo()") int getFoo() {return foo;}
};int main() {Bar bar;bar.setFoo(42);std::cout << "Value of foo: " << bar.getFoo() << std::endl;return 0;
}
  • 在 RFL_CLASS 宏和类定义中,我们添加了一个字符串参数,用于指定类的名称。这样可以在反射时更准确地标识类。
  • 在 RFL_PROPERTY 宏和字段定义中,我们添加了一个字符串参数,用于指定字段的类型和名称。这样可以在反射时更准确地标识字段。
  • 在 RFL_FUNC 宏和成员函数定义中,我们添加了一个字符串参数,用于指定函数的签名。这样可以在反射时更准确地标识函数。

在main函数中,我们创建了一个Bar对象,并使用setFoo和getFoo函数来设置和获取foo字段的值。这些函数的签名和类/字段的信息都被注解添加到了代码中,以便在反射时能够准确地识别和访问它们。

3. 元信息结构反射机制

要在C++中构建一套可以在运行时使用的反射机制,你可以利用模板元编程在编译期生成类型元信息,并在运行时通过这些元信息进行反射操作。

首先,我们需要定义一种数据结构来存储类型的元信息。

#include <iostream>
#include <string>
#include <typeinfo>
#include <typeindex>
#include <unordered_map>
#include <vector>struct FieldInfo {std::string name;std::type_index type;size_t offset;
};struct TypeInfo {std::string name;std::vector<FieldInfo> fields;
};

然后,我们需要一个机制来自动生成这些元信息。这里我们用模板和宏来实现这一点。

#define REFLECTABLE(...) \friend struct Reflection; \static void reflect(Reflection& r) { \r.registerType(typeid(*this), #__VA_ARGS__, __VA_ARGS__); \}class Reflection {
public:template<typename T>void registerType(std::type_index type, const std::string& fieldNames, T& instance) {std::istringstream stream(fieldNames);std::string fieldName;size_t offset = 0;while (std::getline(stream, fieldName, ',')) {trim(fieldName);TypeInfo& typeInfo = typeRegistry[type];typeInfo.name = type.name();typeInfo.fields.push_back({ fieldName, typeid(instance. * (T::*)(T:: *) &fieldName), offset });offset += sizeof(fieldName);}}template<typename T>const TypeInfo& getTypeInfo() {return typeRegistry[std::type_index(typeid(T))];}private:std::unordered_map<std::type_index, TypeInfo> typeRegistry;void trim(std::string& s) {s.erase(0, s.find_first_not_of(' '));s.erase(s.find_last_not_of(' ') + 1);}
};

现在,我们可以定义一个类,并使用宏来使其成为可反射的。

class MyClass {
public:int x;float y;std::string z;REFLECTABLE(x, y, z)
};

4. 使用反射机制

最后,我们可以在运行时使用反射机制来访问类的元信息。

int main() {MyClass obj;Reflection reflection;// 注册类型信息obj.reflect(reflection);// 获取类型信息const TypeInfo& typeInfo = reflection.getTypeInfo<MyClass>();// 输出字段信息std::cout << "Type: " << typeInfo.name << std::endl;for (const auto& field : typeInfo.fields) {std::cout << "Field: " << field.name << ", Type: " << field.type.name() << ", Offset: " << field.offset << std::endl;}return 0;
}
  1. FieldInfo 和 TypeInfo 结构体:这些结构体用于存储字段和类型的元信息。
  2. Reflection 类:这个类负责注册和存储类型元信息,并提供查询接口。
  3. REFLECTABLE 宏:这个宏用于简化类型元信息的注册过程。它声明一个友元函数,这个函数可以访问类的私有成员,并在编译期生成字段的名字和类型信息。
  4. registerType 和 getTypeInfo 方法
    • registerType 方法在编译期处理传入的字段名称,生成对应的字段信息并存储在一个哈希表中。
    • getTypeInfo 方法在运行时查询并返回存储的类型信息。
  5. MyClass 类:这是一个简单的示例类,通过 REFLECTABLE 宏声明它是可反射的。
  6. main 函数:在 main 函数中,我们创建一个 MyClass 的实例并注册它的类型信息,然后查询并输出这些信息。

这个例子展示了如何在 C++ 中利用模板和宏实现一个简单的反射机制。这个机制允许你在运行时访问类型的元信息,从而实现各种动态操作,例如序列化和反序列化、类型检查和动态调用等等。

点击在C++上实现反射用法 查看全文


http://www.ppmy.cn/embedded/140869.html

相关文章

Java面试题、八股文学习之JVM篇

1.你知道哪些垃圾收集算法 垃圾收集&#xff08;Garbage Collection&#xff0c;简称GC&#xff09;是自动内存管理的一部分&#xff0c;它负责回收不再使用的对象所占用的内存空间。不同的编程语言和运行环境可能使用不同的垃圾收集算法。以下是几种常见的垃圾收集算法&#…

在 Mac(ARM 架构)上安装 JDK 8 环境

文章目录 步骤 1&#xff1a;检查系统版本步骤 2&#xff1a;下载支持 ARM 的 JDK 8步骤 3&#xff1a;安装 JDK步骤 4&#xff1a;配置环境变量步骤 5&#xff1a;验证安装步骤 6&#xff1a;注意事项步骤7&#xff1a;查看Java的安装路径 在 Mac&#xff08;ARM 架构&#xf…

docker入门学习笔记

docker的定义 docker是一个用于构建、运行、传送 应用程序的平台。 为什么要使用docker &#xff1f; 在开发测试库环境中测试成功后&#xff0c;打包成集装箱&#xff0c;到生产环境也是能够成功的。而传统的安装方式不仅繁琐&#xff0c;并且在测试环境安装后&#xff0c;到…

关于IDE的相关知识之三【插件安装、配置及推荐的意义】

成长路上不孤单&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a; 【14后&#x1f60a;///C爱好者&#x1f60a;///持续分享所学&#x1f60a;///如有需要欢迎收藏转发///&#x1f60a;】 今日分享关于ide插件安装、配置及推荐意义的相关内容…

【Redis篇】String类型命令详讲以及它的使用场景

目录 前言&#xff1a; 基本命令&#xff1a; setnx/setxx FLUSHALL mest mget 计数命令 INCR / INCRBY DECR/DECYBY INCRBYFLOAT 其他命令 APPEND GETRANGE SETRANGE STRLEN String的典型使用场景 缓存&#xff08;Cache&#xff09;功能 计数&#xff08;…

Python 网络爬虫操作指南

网络爬虫是自动化获取互联网上信息的一种工具。它广泛应用于数据采集、分析以及实现信息聚合等众多领域。本文将为你提供一个完整的Python网络爬虫操作指南&#xff0c;帮助你从零开始学习并实现简单的网络爬虫。我们将涵盖基本的爬虫概念、Python环境配置、常用库介绍。 上传…

display相关的一些知识

1. DSI (MIPI DSI) 的 Video Mode 和 Command Mode MIPI DSI 是一种针对移动设备设计的显示接口&#xff0c;确实具有 Video Mode 和 Command Mode 的概念&#xff0c;这是它的一个显著特点&#xff1a; Video Mode 工作方式&#xff1a;数据流以固定的时间间隔传输帧缓冲内容…

【优先算法学习】双指针--结合题目讲解学习

目录 1.有效三角形的个数 1.2题目解题思路 1.3代码实现 2.和为s的两个数 2.1刷题链接-> 2.2题目解题思路 2.3代码实现 1.有效三角形的个数 1.1刷题链接-> 力扣-有效三角形的个数https://leetcode.cn/problems/valid-triangle-number/description/ 1.2题目解…