本文会展示一个小巧灵活的系统,用于使用C ++ 11语言功能进行运行时反射。这是一个为C ++类型生成元数据的系统。元数据采用TypeDescriptor在运行时创建的对象的形式,该对象描述其他运行时对象的结构。
我将这些对象称为类型描述符。我写这个反射系统的最初动机是为了支持序列化用于一个C ++游戏引擎,因为有非常具体的需求,一旦可行,我也开始将运行时反射用于其他引擎功能:
该反射系统基于预处理器宏和模板。至少以当前形式,C ++并非旨在使运行时反射变得容易。众所周知,编写一个易于使用,易于扩展且切实可行的反射系统非常困难。在安顿我今天拥有的系统之前,我被模糊的语言规则,初始化顺序的错误和极端的情况困扰了很多次。
为了说明其工作原理,我在GitHub上发布了一个示例项目:(代码贴在文末处)
该示例实际上并未使用我的游戏引擎的反射系统。而是使用了自带的微型反射系统,但类型描述符的创建,结构化和寻找方式几乎相同。这就是我将在这篇文章中重点讨论的部分。
本文供那些对如何开发运行时反射系统感兴趣的程序员,而不仅仅是使用涉及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对象看起来像这样:
接下来,该示例找到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);
运行的输出为:
将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都有一个名称,大小和几个虚拟函数:
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用红色圈出了各个子类:
在运行时,可以通过调用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