在本文中,我将使用 C++ 11 的语言特性完成一个轻量,灵活的运行时反射 系统。这是一个为 C++ 类型生成元数据 (metadata) 的系统。元数据采用 TypeDescriptor
对象的形式,在运行时创建,用于描述其他运行时对象的结构。
我将把这些对象称为类型描述符 。我编写反射系统的最初动机是在我自定义的 C++ 游戏引擎中支持序列化,因为我有非常特殊的需求。一旦这个方法奏效,我便开始在其他引擎功能中使用运行时反射:
- 3D rendering:每次游戏引擎使用 OpenGL ES 绘制内容时,它都会使用反射将统一参数 (uniform parameters) 和描述顶点格式 (vertex formats) 传递给 API。它使图形编程更富有成效!
- Importing JSON:引擎的资产管道有一个通用例程,用于从 JSON 文件和类型描述符合成 C++ 对象。它用于导入 3D 模型、关卡定义和其他资产。
该反射系统基于预处理宏和模版。在当前的 C++ 版本中,实现运行时反射并不是一件容易的事情。任何编写过反射系统的人都知道,设计一个易于使用,易于扩展且实际有效的反射系统是很困难的。在确定今天的系统之前,我曾多次被模糊的语言规则,初始化顺序和极端情况所困扰。
为了说明它是如何工作的,我发布了一个示例项目在 Github:
这个示例实际上并没有使用我游戏引擎的反射系统。它使用了自己的一个小反射系统,但最有趣的部分------类型描述符的创建 (created) ,结构 (structured) 和 查找 (found) ------ 几乎是相同的。这些是我将在本篇文章中重点讨论的部分。在下一篇文章中,我将讨论如何扩展该系统。
这篇文章是为那些对如何开发运行时反射系统感兴趣的程序员写的。它涉及到 C++ 的许多高级特性,但是样例项目只有 242 行代码,所以希望有恒心,有决心的 C++ 程序员都能跟上。如果你对使用现有的解决方案更感兴趣,请查看 RTTR。
展示
在 Main.cpp 中,示例项目定义了一个名为 Node
的结构体。REFLECT()
宏告诉系统为这种类型启用反射。
c++
struct Node {
std::string key;
int value;
std::vector<Node> children;
REFLECT(); // Enable reflection for this type
};
在运行时,示例创建了一个类型为 Node
的对象。
c++
// Create an object of type Node
Node node = {"apple", 3, {{"banana", 7, {}}, {"cherry", 11, {}}}};
在内存中,Node
对象如下所示:
之后,示例查找 Node
的类型描述符。要使其工作,必须将以下宏放在 .cpp 文件的某个位置。我把它们放在 Main.cpp 中,但是它们也可以放在任何可以看到 Node
定义的文件中。
c++
// Define Node's type descriptor
REFLECT_STRUCT_BEGIN(Node)
REFLECT_STRUCT_MEMBER(key)
REFLECT_STRUCT_MEMBER(value)
REFLECT_STRUCT_MEMBER(children)
REFLECT_STRUCT_END()
Node
的成员变量现在被称为反射 (reflected) 。
一个指向 Node
类型描述符的指针是通过调用 reflect::TypeResolver<Node>::get()
:
c++
// Find Node's type descriptor
reflect::TypeDescriptor* typeDesc = reflect::TypeResolver<Node>::get();
找到类型描述符后,示例使用它将 Node
对象的描述输出到控制台。
c++
// Dump a description of the Node object to the console
typeDesc->dump(&node);
输出如下:
宏是如何实现的
当你将 REFLECT()
宏添加到结构体或类中时,它声明了两个额外的静态成员:Reflection
(结构体的类型描述符) 和 initReflection
(初始化类型描述符的函数)。实际上,当宏展开时,完整的 Node
结构看起来像这样:
c++
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*);
};
类似地,Main.cpp 中的 REFLECT_STRUCT_*()
宏在展开时是这样的:
c++
// Definition of the struct's type descriptor:
reflect::TypeDescriptor_Struct Node::Reflection{Node::initReflection};
// Definition of the function that initializes it:
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 类型别名的地方:使用 T = Node
。如果没有类型别名,我们必须将标识符 Node
作为额外的参数传递给每个 REFLECT_STRUCT_MEMBER()
宏。宏就不那么容易使用了。
如你所见,在函数内部,有三个额外的函数调用 reflect::TypeResolver<>::get()
。每个函数都为 Node
的反射成员找到类型描述符。这些调用使用 C++ 11 的 decltype
说明符自动将正确的类型传递给 TypeResolver
模版。
查找 TypeDescriptors
TypeResolver
是一个类模版。当你为特定类型 T 调用 TypeResolver<T>::get()
时,编译器实例化一个函数,该函数为 T
返回相应的TypeDescriptor
。它服务于反射结构以及这些结构的反射成员。
默认情况,如果T
是一个包含 REFLECT()
宏的结构体(或者类),像Node
,get()
将返回一个指向该结构体 Reflection
成员的指针。对于其他类型T
,get()
调用getPrimitiveDescriptor<T>
------一个处理int
或std::string
等基本类型的函数模版。
c++
// Declare the function template that handles primitive types such as int, std::string, etc.:
template <typename T>
TypeDescriptor* getPrimitiveDescriptor();
// A helper class to find TypeDescriptors in different ways:
struct DefaultResolver {
...
// This version is called if T has a static member variable named "Reflection":
template <typename T, /* SFINAE stuff here */>
static TypeDescriptor* get() {
return &T::Reflection;
}
// This version is called otherwise:
template <typename T, /* SFINAE stuff here */>
static TypeDescriptor* get() {
return getPrimitiveDescriptor<T>();
}
};
// This is the primary class template for finding all TypeDescriptors:
template <typename T>
struct TypeResolver {
static TypeDescriptor* get() {
return DefaultResolver::get<T>();
}
};
这有一点编译时逻辑------根据T
中是否存在静态成员变量生成不同的代码------使用 SFINAE 实现的。我从上面的代码片段中省略了 SFINAE 代码,因为坦率地说,它很丑。它的一部分可以用if constexpr
更优雅地重写,但我的目标是 C++11。即使这样,检测 T 是否具有特定成员变量的部分仍然很难看,至少在 C++ 实现静态反射之前是这样。
TypeDescriptors 的结构
在样例项目中,每个TypeDescriptor
都有 name,size,和两个虚函数。
c++
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*
。
c++
virtual void dump(const void* obj, int /*unused*/) const override {
std::cout << "std::string{\"" << *(const std::string*) obj << "\"}";
}
你可能想知道以这种方式强制转换 void 指针是否安全。显然,如果传入一个无效的指针,程序很可能崩溃。这就是为什么在我的游戏引擎中,由 void 指针表示的对象总是带着它们的类型描述符成对移动。通过以这种方式表示对象,可以编写多种泛型算法。
在样例项目中,将对象打印输出到控制台是唯一的功能,但是你可以想象类型描述符如何作为序列化为二进制格式的框架。
在下一篇文章中,我将解释如何向反射系统添加内置类型,以及上图中的"匿名函数"(anonymous functions) 是干什么用的。我还将讨论扩展该系统的其他方法。