新手友好 | 在C++上实现反射用法

科技   2024-11-17 17:20   湖北  

0. 简介


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



1. 反射的用途


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


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


1、静态反射,就是在编译期生成反射信息

2、动态反射,就是在运行时生成反射信息

3、动态反射,显然需要一套强大的运行时和动态类型系统,也是显然的很复杂


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


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


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


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


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


第五种: 绑架编译器! clang提供了插件功能。(https://clang.llvm.org/docs/ClangPlugins.html)事实上也有大佬做了,这些都有比较详细的例子。(https://github.com/matus-chochlik/llvm-project/tree/reflection)



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; }};
// 注册反射类 ARegisterClass(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; }};
// 注册反射类 BRegisterClass(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; }};
// 注册反射类 CRegisterClass(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(https://blog.csdn.net/qq_37286579/article/details/130266282)((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字段的值。这些函数的签名和类/字段的信息都被注解添加到了代码中,以便在反射时能够准确地识别和访问它们。



4. 元信息结构反射机制


要在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)};


 使用反射机制


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


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++ 中利用模板和宏实现一个简单的反射机制。这个机制允许你在运行时访问类型的元信息,从而实现各种动态操作,例如序列化和反序列化、类型检查和动态调用等等。



5. 调试器的运行时信息形成反射


使用调试器的运行时信息来生成反射代码是一种非常高级的技术,通常需要结合调试器提供的 API 和脚本语言(例如 Python)进行自动化处理。这个过程大致可以分为以下几个步骤:


1、使用调试器获取目标程序的运行时信息。

2、解析并提取元信息。

3、生成对应的反射代码。


首先,编写一个简单的 C++ 程序:


// example.cpp#include <iostream>#include <vector>#include <string>
class MyClass {public:    int x;    float y;    std::string z;
   void print() {        std::cout << "x: " << x << ", y: " << y << ", z: " << z << std::endl;    }};
int main() {    MyClass obj;    obj.x = 10;    obj.y = 20.5f;    obj.z = "Hello, World!";    obj.print();    return 0;}


编译这个程序:


g++ -g example.cpp -o example


下面编写一个 Python 脚本,通过 GDB 获取运行时信息。这次我们会坚持在主要代码中使用这些信息,而不生成额外的 C++ 文件。


# extract_info.pyimport gdb
class ExtractInfo(gdb.Command):    def __init__(self):        super(ExtractInfo, self).__init__("extract_info", gdb.COMMAND_USER)
   def invoke(self, arg, from_tty):        obj = gdb.parse_and_eval("obj")        obj_type = obj.type        print(f"Class: {obj_type.name}")        fields = []        for field in obj_type.fields():            fields.append((field.name, field.type, field.bitpos // 8))        self.generate_reflection_code(obj_type.name, fields)
   def generate_reflection_code(self, class_name, fields):        code = f"struct FieldInfo {{\n    std::string name;\n    std::string type;\n    size_t offset;\n}};\n"        code += f"struct TypeInfo {{\n    std::string name;\n    std::vector<FieldInfo> fields;\n}};\n"        code += f"TypeInfo get{class_name}TypeInfo() {{\n"        code += f"    TypeInfo typeInfo = {{\"{class_name}\", {{\n"        for name, type_, offset in fields:            code += f"        {{\"{name}\", \"{type_}\", {offset}}},\n"        code += "    }}};\n"        code += "    return typeInfo;\n}\n"        print("\nGenerated code:\n")        print(code)
ExtractInfo()


然后在 GDB 中运行此脚本:


gdb -q example


在 GDB 中:


(gdb) source extract_info.py(gdb) break main(gdb) run(gdb) extract_info


执行以上命令后,你会看到类似以下输出:


Class: MyClassGenerated code:
struct FieldInfo {    std::string name;    std::string type;    size_t offset;};struct TypeInfo {    std::string name;    std::vector<FieldInfo> fields;};TypeInfo getMyClassTypeInfo() {    TypeInfo typeInfo = {"MyClass", {        {"x", "int", 0},        {"y", "float", 4},        {"z", "std::string", 8},    }};    return typeInfo;}


现在我们将根据上面生成的代码,手动将这些元信息加入到程序中,并通过它实现简单的反射。


由于我们希望不生成额外的 C++ 文件,直接将上面生成的代码加入到现有的 example.cpp 中:


#include <iostream>#include <vector>#include <string>
// 基础反射结构定义,根据生成的代码添加struct FieldInfo {    std::string name;    std::string type;    size_t offset;};
struct TypeInfo {    std::string name;    std::vector<FieldInfo> fields;};
TypeInfo getMyClassTypeInfo() {    TypeInfo typeInfo = {"MyClass", {        {"x", "int", 0},        {"y", "float", 4},        {"z", "std::string", 8},    }};    return typeInfo;}
class MyClass {public:    int x;    float y;    std::string z;
   void print() {        std::cout << "x: " << x << ", y: " << y << ", z: " << z << std::endl;    }};
int main() {    MyClass obj;    obj.x = 10;    obj.y = 20.5f;    obj.z = "Hello, World!";    obj.print();
   // 使用反射信息输出类的字段信息    const TypeInfo& typeInfo = getMyClassTypeInfo();    std::cout << "Type: " << typeInfo.name << std::endl;    for (const auto& field : typeInfo.fields) {        std::cout << "Field: " << field.name << ", Type: " << field.type << ", Offset: " << field.offset << std::endl;    }
   return 0;}


编译并运行这个更新后的example.cpp:


g++ example.cpp -o example./example


输出应该如下所示:


x: 10, y: 20.5, z: Hello, World!Type: MyClassField: x, Type: int, Offset: 0Field: y, Type: float, Offset: 4Field: z, Type: std::string, Offset: 8


详细解释


1、Python 脚本:

创建一个新的GDB命令extract_runtime_info。

解析并评估 obj 对象,获取其类型信息。

遍历类型的字段并输出字段名和类型。


2、生成的 C++ 反射代码:

用获取到的类型和字段信息生成 TypeInfo  FieldInfo 结构。


实现一个 MyClassReflection 类来返回静态的类型信息。


 main 函数中,使用反射信息输出 MyClass 类型的字段信息。



6. 绑架编译器


绑架编译器来形成反射机制通常涉及利用编译器的扩展功能或构建工具链来自动提取或生成元信息。实际应用中,可以使用诸如Clang的AST插件、libclang库、或其他编译时工具。这种方式允许在编译过程中自动生成元信息,从而避免手工编写反射代码。以下是一个利用Clang工具链实现反射机制的示例。


6.1 安装Clang和libclang


首先,确保你已经安装了Clang和libclang。你可以使用以下命令在Ubuntu系统上安装:


sudo apt-get install clang libclang-dev


6.2 编写Clang插件或工具


利用Clang AST(抽象语法树)来提取C++类的元信息,并生成相应的反射代码。下面是一个示例,展示如何使用Clang工具来实现这一目标。


编写一个基于Clang的工具来提取类和字段的元信息。假设我们将这个工具命名为reflect_tool.cpp。


#include <clang/AST/AST.h>#include <clang/AST/ASTConsumer.h>#include <clang/AST/RecursiveASTVisitor.h>#include <clang/Frontend/FrontendActions.h>#include <clang/Frontend/CompilerInstance.h>#include <clang/Tooling/CommonOptionsParser.h>#include <clang/Tooling/Tooling.h>
using namespace clang;using namespace clang::tooling;
class ReflectASTVisitor : public RecursiveASTVisitor<ReflectASTVisitor> {public:    explicit ReflectASTVisitor(ASTContext *Context) : Context(Context) {}
   bool VisitCXXRecordDecl(CXXRecordDecl *Declaration) {        if (Declaration->isThisDeclarationADefinition()) {            llvm::outs() << "Class: " << Declaration->getNameAsString() << "\n";            for (auto Field : Declaration->fields()) {                llvm::outs() << "  Field: " << Field->getNameAsString()                            << ", Type: " << Field->getType().getAsString()                            << ", Offset: " << Context->getFieldOffset(Field) / 8                            << "\n";            }        }        return true;    }
private:    ASTContext *Context;};
class ReflectASTConsumer : public ASTConsumer {public:    explicit ReflectASTConsumer(ASTContext *Context) : Visitor(Context) {}
   void HandleTranslationUnit(ASTContext &Context) override {        Visitor.TraverseDecl(Context.getTranslationUnitDecl());    }
private:    ReflectASTVisitor Visitor;};
class ReflectFrontendAction : public ASTFrontendAction {public:    std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef file) override {        return std::make_unique<ReflectASTConsumer>(&CI.getASTContext());    }};
static llvm::cl::OptionCategory ReflectToolCategory("reflect-tool options");
int main(int argc, const char **argv) {    CommonOptionsParser OptionsParser(argc, argv, ReflectToolCategory);    ClangTool Tool(OptionsParser.getCompilations(), OptionsParser.getSourcePathList());    return Tool.run(newFrontendActionFactory<ReflectFrontendAction>().get());}


此工具将提取C++类的元信息并输出。


6.2.1 编译Clang工具


clang++ -std=c++11 -o reflect_tool reflect_tool.cpp `llvm-config --cxxflags --ldflags --system-libs --libs all`


6.3 使用Clang工具提取元信息


我们使用上面编写并编译的工具来提取一个示例C++文件的元信息。


6.3.1 创建C++源文件


创建一个简单的C++类文件,例如example.cpp


// example.cpp#include <iostream>#include <string>
class MyClass {public:    int x;    float y;    std::string z;
   void print() {        std::cout << "x: " << x << ", y: " << y << ", z: " << z << std::endl;    }};


6.3.2 运行Clang工具


使用Clang工具提取元信息:


./reflect_tool example.cpp


输出应如下所示:


Class: MyClass  Field: x, Type: int, Offset: 0  Field: y, Type: float, Offset: 4  Field: z, Type: std::string, Offset: 8



7. 参考论文


https://feiqi3.cn/blog/124


https://github.com/AngryHacker/articles/blob/master/src/c_plus_cplus/reflection_in_c%2B%2B_3.md



讲师招募


招募要求

完成符合要求的机器人相关视频制作

总时长需达到 3小时以上

视频内容需为精品课程,确保高质量和专业性


讲师奖励

享受课程收入分成

赠送 2门 古月学院在售精品课程(训练营除外)


联系我们

添加工作人员微信:GYH-xiaogu




古月居
专业的ROS机器人知识社区和产业服务平台
 最新文章