您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > C/C++/C#

灵活的使用C ++中的反射系统

时间:2020-04-21 11:09:01  来源:  作者:

本文会展示一个小巧灵活的系统,用于使用C ++ 11语言功能进行运行时反射。这是一个为C ++类型生成元数据的系统。元数据采用TypeDescriptor在运行时创建的对象的形式,该对象描述其他运行时对象的结构。

灵活的使用C ++中的反射系统

 

我将这些对象称为类型描述符。我写这个反射系统的最初动机是为了支持序列化用于一个C ++游戏引擎,因为有非常具体的需求,一旦可行,我也开始将运行时反射用于其他引擎功能:

  • 3D渲染:每次游戏引擎使用OpenGL ES进行绘制时,它都会使用反射来传递统一的参数并向API描述顶点格式,这样会使图形编程更加高效!
  • 导入JSON:引擎的管道具备通用例程,可以从JSON文件和类型描述符合成C ++对象。它用于导入3D模型,关卡定义和其他应用。

该反射系统基于预处理器宏和模板。至少以当前形式,C ++并非旨在使运行时反射变得容易。众所周知,编写一个易于使用,易于扩展且切实可行的反射系统非常困难。在安顿我今天拥有的系统之前,我被模糊的语言规则,初始化顺序的错误和极端的情况困扰了很多次。

为了说明其工作原理,我在GitHub上发布了一个示例项目:(代码贴在文末处)

灵活的使用C ++中的反射系统

 

该示例实际上并未使用我的游戏引擎的反射系统。而是使用了自带的微型反射系统,但类型描述符的创建结构化寻找方式几乎相同。这就是我将在这篇文章中重点讨论的部分。

本文供那些对如何开发运行时反射系统感兴趣的程序员,而不仅仅是使用涉及C ++的高级功能,但是示例项目只有242行代码,因此希望能够持续迭代,任何C ++程序员都提交代码,如果您对使用现有解决方案更感兴趣,请查看RTTR。

示例

在中main.cpp,示例项目定义了一个名为的结构Node。该REFLECT()宏告诉系统,以使这种类型的反射。

struct Node {
    std::string key;
    int value;
    std::vector<Node> children;

    REFLECT()      // 此类型启用反射
};

在运行时,该示例创建一个类型为的对象Node。

//创建类型为Node的对象 
Node node = {"Apple", 3, {{"banana", 7, {}}, {"cherry", 11, {}}}};

内存中,Node对象看起来像这样:

灵活的使用C ++中的反射系统

 

接下来,该示例找到Node的类型描述符。为此,必须将以下宏放在main.cpp文件中的某个位置。我将它们放在中Main.cpp,但也可将它们放在Node可见其定义的任何文件中。

//定义节点的类型描述符
REFLECT_STRUCT_BEGIN(Node)
REFLECT_STRUCT_MEMBER(key)
REFLECT_STRUCT_MEMBER(value)
REFLECT_STRUCT_MEMBER(children)
REFLECT_STRUCT_END()

Node现在据说可以反映其成员变量

Node可以通过调用获得指向类型描述符的指针reflect::TypeResolver<Node>::get():

//查找Node的类型描述符
reflect::TypeDescriptor* typeDesc = reflect::TypeResolver<Node>::get();

找到类型描述符后,该示例将其用于将Node对象的描述转储到控制台。

//将Node对象的描述转储到控制台
typeDesc->dump(&node);

运行的输出为:

灵活的使用C ++中的反射系统

 

宏的实现方式

将REFLECT()宏添加到结构或类时,它会声明两个其他静态成员:Reflection,结构的类型描述符和initReflection初始化它的函数。实际上,展开宏后,完整的Node结构如下所示:

struct Node {
    std::string key;
    int value;
    std::vector<Node> children;

    // Declare the struct's type descriptor:
    static reflect::TypeDescriptor_Struct Reflection;

    // Declare a function to initialize it:
    static void initReflection(reflect::TypeDescriptor_Struct*);
};

同样,展开后的REFLECT_STRUCT_*()宏块main.cpp如下所示:

//定义结构类型描述符: 
reflect::TypeDescriptor_Struct Node::Reflection{Node::initReflection};

//初始化它的函数的定义:
void Node::initReflection(reflect::TypeDescriptor_Struct* typeDesc) {
    using T = Node;
    typeDesc->name = "Node";
    typeDesc->size = sizeof(T);
    typeDesc->members = {
        {"key", offsetof(T, key), reflect::TypeResolver<decltype(T::key)>::get()},
        {"value", offsetof(T, value), reflect::TypeResolver<decltype(T::value)>::get()},
        {"children", offsetof(T, children), reflect::TypeResolver<decltype(T::children)>::get()},
    };
}

现在,由于Node::Reflection是静态成员变量,因此initReflection()在程序启动时会自动调用其构造函数,该构造函数接受指向的指针。您可能想知道:为什么将函数指针传递给构造函数?为什么不通过初始化列表呢?答案是因为函数的主体为我们提供了声明C ++ 11 类型别名的位置:using T = Node。没有类型别名,所以我们必须将标识符Node作为额外的参数传递给每个REFLECT_STRUCT_MEMBER()宏。

如上所示,在函数内部还有另外三个对的调用reflect::TypeResolver<>::get()。每个人都会找到的反映成员的类型描述符Node。这些调用使用C ++ 11的decltype说明符自动将正确的类型传递给TypeResolver模板。

查找类型描述符

(请注意,本节中的所有内容都在reflect名称空间中定义。)

TypeResolver是一个类模板。当你调用TypeResolver<T>::get()特定类型T,编译器实例化相应的回报功能TypeDescriptor的T。它适用于反射结构以及这些结构的每个反射成员。默认情况下,这是通过主模板进行的,如下所示。

默认情况下,如果T是包含REFLECT()宏之类的结构(或类),如Node中get()将返回指向该结构Reflection成员的指针-这就是我们想要的。对于其他所有类型T,get()则调用getPrimitiveDescriptor<T>处理原始类型的函数模板,例如intstd::string

//声明处理原始类型(例如int,std :: string等)的函数模板:
template <typename T>
TypeDescriptor* getPrimitiveDescriptor();

//以不同方式查找TypeDescriptor的帮助器类:
struct DefaultResolver {
    ...

    //如果T具有名为“ Reflection”的静态成员变量,则调用此版本:
    template <typename T, /* 在SFINAE 处 */>
    static TypeDescriptor* get() {
        return &T::Reflection;
    }

    //否则称为此版本:
    template <typename T, /* 在SFINAE 处 */>
    static TypeDescriptor* get() {
        return getPrimitiveDescriptor<T>();
    }
};

//这是查找所有TypeDescriptor的主要类模板:
template <typename T>
struct TypeResolver {
    static TypeDescriptor* get() {
        return DefaultResolver::get<T>();
    }
};

T使用SFINAE可以实现这一点的编译时逻辑:根据是否存在静态成员变量生成不同的代码。我从上面的代码段中省略了SFINAE代码,坦率来讲,看上去虽然很丑陋,但是可以在源代码中检查实际的实现。使用可以更优雅地重写其中的一部分if constexpr,但是我的目标是C ++ 11。即使这样,T至少可以在C ++采用静态反射之前,检测是否具有特定成员变量的部分。

TypeDescriptor的结构

在示例项目中,每个项目TypeDescriptor都有一个名称,大小和几个虚拟函数:

struct TypeDescriptor {
    const char* name;
    size_t size;

    TypeDescriptor(const char* name, size_t size) : name{name}, size{size} {}
    virtual ~TypeDescriptor() {}
    virtual std::string getFullName() const { return name; }
    virtual void dump(const void* obj, int indentLevel = 0) const = 0;
};

示例项目永远不会TypeDescriptor直接创建对象。而是由系统创建从派生的类型的对象TypeDescriptor。这样一来,每个类型描述符可以根据容纳额外的信息。

例如,返回的对象的实际类型TypeResolver<Node>::get()为TypeDescriptor_Struct。它有一个附加的成员变量,members其中包含有关的每个反映成员的信息Node。对于每个反映成员,都有一个指向另一个成员的指针TypeDescriptor,整个过程在内存中就是这样子。我在TypeDescriptor用红色圈出了各个子类:

灵活的使用C ++中的反射系统

 

在运行时,可以通过调用getFullName()其类型描述符来获取任何类型的全名。大多数子类仅使用getFullName()返回的基类实现TypeDescriptor::name。在此示例中,唯一的例外是采用TypeDescriptor_StdVector描述std::vector<>专业化的子类。为了返回完整的类型名称,例如"std::vector<Node>",它保留了指向其项目类型的类型描述符的指针。我们可以在上面的内存图中看到这一点:有一个TypeDescriptor_StdVector对象,其itemType成员一直指向的类型描述符Node。

当然,类型描述符仅描述类型。为了对运行时对象进行完整的描述,我们既需要类型描述符,也需要指向对象本身的指针。

请注意,TypeDescriptor::dump()该对象接受指向的指针const void*。这是因为抽象TypeDescriptor接口要能处理任何对象类型,子类实现只需要确定其所需的类型即可。例如,这个实现TypeDescriptor_StdString::dump()。将转换const void*为const std::string*。

virtual void dump(const void* obj, int /*unused*/) const override {
    std::cout << "std::string{"" << *(const std::string*) obj << ""}";
}

至于,以void这种方式强制转换指针是否安全。显然,如果传入了无效的指针,则程序很可能崩溃。这就是为什么在我的游戏引擎中,void指针所代表的对象总是与它们的类型描述符成对出现。通过以这种方式表示对象,可以编写多种通用算法。

在示例项目中,是将对象转储到控制台输出实现功能的,其实我们还能实现将对象类型描述符运用作二进制格式的序列化框架中。

传送门

阅读详细代码,请移步:https://github.com/preshing/FlexibleReflection/tree/part1



Tags:C ++ 反射系统   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
本文会展示一个小巧灵活的系统,用于使用C ++ 11语言功能进行运行时反射。这是一个为C ++类型生成元数据的系统。元数据采用TypeDescriptor在运行时创建的对象的形式,该对象描...【详细内容】
2020-04-21  Tags: C ++ 反射系统  点击:(79)  评论:(0)  加入收藏
▌简易百科推荐
一、简介很多时候我们都需要用到一些验证的方法,有时候需要用正则表达式校验数据时,往往需要到网上找很久,结果找到的还不是很符合自己想要的。所以我把自己整理的校验帮助类分...【详细内容】
2021-12-27  中年农码工    Tags:C#   点击:(2)  评论:(0)  加入收藏
引言在学习C语言或者其他编程语言的时候,我们编写的一个程序代码,基本都是在屏幕上打印出 hello world ,开始步入编程世(深)界(坑)的。C 语言版本的 hello world 代码:#include <std...【详细内容】
2021-12-21  一起学嵌入式    Tags:C 语言   点击:(11)  评论:(0)  加入收藏
读取SQLite数据库,就是读取一个路径\\192.168.100.**\position\db.sqlite下的文件<startup useLegacyV2RuntimeActivationPolicy="true"> <supportedRuntime version="v4.0"/...【详细内容】
2021-12-16  今朝我的奋斗    Tags:c#   点击:(21)  评论:(0)  加入收藏
什么是shellshell是c语言编写的程序,它在用户和操作系统之间架起了一座桥梁,用户可以通过这个桥梁访问操作系统内核服务。 它既是一种命令语言,同时也是一种程序设计语言,你可以...【详细内容】
2021-12-16  梦回故里归来    Tags:shell脚本   点击:(18)  评论:(0)  加入收藏
一、编程语言1.根据熟悉的语言,谈谈两种语言的区别?主要浅谈下C/C++和PHP语言的区别:1)PHP弱类型语言,一种脚本语言,对数据的类型不要求过多,较多的应用于Web应用开发,现在好多互...【详细内容】
2021-12-15  linux上的码农    Tags:c/c++   点击:(17)  评论:(0)  加入收藏
1.字符串数组+初始化char s1[]="array"; //字符数组char s2[6]="array"; //数组长度=字符串长度+1,因为字符串末尾会自动添&lsquo;\0&lsquo;printf("%s,%c\n",s1,s2[2]);...【详细内容】
2021-12-08  灯-灯灯    Tags:C语言   点击:(47)  评论:(0)  加入收藏
函数调用约定(Calling Convention),是一个重要的基础概念,用来规定调用者和被调用者是如何传递参数的,既调用者如何将参数按照什么样的规范传递给被调用者。在参数传递中,有两个很...【详细内容】
2021-11-30  小智雅汇    Tags:函数   点击:(19)  评论:(0)  加入收藏
一、问题提出问题:把m个苹果放入n个盘子中,允许有的盘子为空,共有多少种方法?注:5,1,1和1 5 1属同一种方法m,n均小于10二、算法分析设f(m,n) 为m个苹果,n个盘子的放法数目,则先对...【详细内容】
2021-11-17  C语言编程    Tags:C语言   点击:(49)  评论:(0)  加入收藏
一、为什么需要使用内存池在C/C++中我们通常使用malloc,free或new,delete来动态分配内存。一方面,因为这些函数涉及到了系统调用,所以频繁的调用必然会导致程序性能的损耗;另一...【详细内容】
2021-11-17  深度Linux    Tags:C++   点击:(38)  评论:(0)  加入收藏
OpenCV(Open Source Computer Vision Library)是一个(开源免费)发行的跨平台计算机视觉库,可以运行在Linux、Windows、Android、ios等操作系统上,它轻量级而且高效---由一系列...【详细内容】
2021-11-11  zls315    Tags:C#   点击:(50)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条