在C++上实现反射用法
0. 简介
最近看很多端到端的工作,发现大多数都是基于mmdet3d来做的,而这个里面用的比较多的形式就是反射机制,这样其实可以比较好的通过类似plugin的形式完成模型模块的插入。当然我们这里不是来分析python的反射机制的。我们这篇文章主要来介绍C++上实现反射。
1. 反射的用途
一般来说就是序列化反序列化啦。比如说你想通过网络传递一个实例,或者把它保存到文件里,以后再取出来放到程序里,这就需要反射
反射其实还能细分为静态反射和动态反射
- 静态反射,就是在编译期生成反射信息
- 动态反射,就是在运行时生成反射信息
- 动态反射,显然需要一套强大的运行时和动态类型系统,也是显然的很复杂
在来,还有侵入式非侵入式之分。非侵入式的反射允许不对源码进行修改就能实现反射;侵入式呢就得对源码动动手脚了。
第一种:实现思路,是在源码里加入大量的反射信息,手动注册反射。 这种库的代表是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
方法会被调用,我们可以在这里生成反射信息并注册类。下面我们对上面代码中主要部分进行详细解释:
-
ReflectionGenerator类:这是一个继承自
MatchFinder::MatchCallback
的自定义类,用于处理匹配到的AST节点。在run
方法中,根据匹配到的C++类定义,获取类名并输出到控制台,同时遍历类的字段并输出字段名。 -
main函数:
- 创建
CommonOptionsParser
对象和ClangTool
对象,用于解析命令行参数和运行Clang工具。 - 创建
ReflectionGenerator
对象和MatchFinder
对象,用于注册匹配规则和处理匹配结果。 - 通过
Tool.run
方法运行Clang工具,并传入匹配规则和处理结果的工厂对象。
- 创建
-
cxxRecordDecl matcher:使用
Finder.addMatcher
添加了一个匹配规则,用于匹配C++类的定义。当匹配到符合规则的AST节点时,会调用ReflectionGenerator
的run
方法进行处理。 -
反射信息生成:在
ReflectionGenerator
的run
方法中,获取到类名和字段名后,输出到控制台。这里展示了获取类名和字段名的基本操作,您可以根据需求进一步扩展生成反射信息的逻辑。
此外这里我们可以通过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;
}
- FieldInfo 和 TypeInfo 结构体:这些结构体用于存储字段和类型的元信息。
- Reflection 类:这个类负责注册和存储类型元信息,并提供查询接口。
- REFLECTABLE 宏:这个宏用于简化类型元信息的注册过程。它声明一个友元函数,这个函数可以访问类的私有成员,并在编译期生成字段的名字和类型信息。
- registerType 和 getTypeInfo 方法:
registerType
方法在编译期处理传入的字段名称,生成对应的字段信息并存储在一个哈希表中。getTypeInfo
方法在运行时查询并返回存储的类型信息。
- MyClass 类:这是一个简单的示例类,通过
REFLECTABLE
宏声明它是可反射的。 - main 函数:在
main
函数中,我们创建一个MyClass
的实例并注册它的类型信息,然后查询并输出这些信息。
这个例子展示了如何在 C++ 中利用模板和宏实现一个简单的反射机制。这个机制允许你在运行时访问类型的元信息,从而实现各种动态操作,例如序列化和反序列化、类型检查和动态调用等等。
点击在C++上实现反射用法 查看全文