在C++上实现反射用法

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++上实现反射用法 查看全文

相关推荐
尘浮生3 分钟前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
hopetomorrow16 分钟前
学习路之PHP--使用GROUP BY 发生错误 SELECT list is not in GROUP BY clause .......... 解决
开发语言·学习·php
不是二师兄的八戒26 分钟前
本地 PHP 和 Java 开发环境 Docker 化与配置开机自启
java·docker·php
小牛itbull26 分钟前
ReactPress vs VuePress vs WordPress
开发语言·javascript·reactpress
怀澈12228 分钟前
高性能服务器模型之Reactor(单线程版本)
linux·服务器·网络·c++
请叫我欧皇i35 分钟前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
闲暇部落38 分钟前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
爱编程的小生38 分钟前
Easyexcel(2-文件读取)
java·excel
GIS瞧葩菜1 小时前
局部修改3dtiles子模型的位置。
开发语言·javascript·ecmascript
chnming19871 小时前
STL关联式容器之set
开发语言·c++