一个糟糕但仍然有用的C++反射系统






4.50/5 (2投票s)
C++反射变得简单
引言
C++缺失的关键功能之一是反射,您知道的。通常,想在项目中利用反射的人必须开发自己的系统,而这些系统通常是应用程序特定的,或者不与开源社区共享,导致人们一次又一次地重复造轮子。
在此,我提出一个C++反射库,它简单易用,功能强大,足以支持您几乎所有可能的需求。该库以许可的开源许可证(BSD-2-Clause)发布。
它是如何开始的
我也是那样开始的,因为作为每个C++和视频游戏爱好者,我想要编写的第一个严肃的应用程序是一个游戏框架。所以我开始边编写框架边编写我的反射系统。
但是由于我没有足够的时间,并且在网上找到了许多强大而完整的开源游戏框架/引擎,所以我放弃了这个项目。然而,我从未找到一个令人满意的C++反射系统,一个只使用C++的系统,因此它既可移植,又简单且足够灵活。我想要尽可能好地学习C++,所以我接受了挑战!
我多年前就开始了这个名为eXtendedMirror
的项目,随着我对该语言知识的提高,它经历了很多次重新设计。我觉得这个项目终于可用了,即使它尚未功能完备,所以我想告诉全世界关于它的信息,并分享一些关于它的用法和设计选择的细节。
有什么糟糕之处?
尽管该系统试图尽可能通用、干净和强大,但它在我看来是对语言本身应该内置的缺失功能的糟糕的变通方法,希望未来会实现。然而,在此期间,我认为它可能非常有用,所以如果您正在寻找类似的东西,您可以尝试一下,甚至可以贡献而不是自己编写。
您可以在这里获取代码。文档虽然尚未完成,但可以通过Doxygen生成。
支持什么
您可能会问“扩展”代表什么。它意味着,不仅支持语言的几乎所有构造,而且还添加了一些构造,例如属性。属性是一种抽象,可以在类实例中表示许多事物。它可以是一个字段,一对get
和set
方法,或者只有一个get
方法。
所有属性都通过系统API以相同的方式访问。
您可以反射带有方法和属性的类、非成员函数、常量、enum
、全局变量、命名空间和模板。继承和多重继承也受支持。
代码使用
我们可以从如何反射一个类开始。请注意,默认情况下,只有public
接口可以被反射。这强制了信息隐藏,并使反射机制是非侵入式的。如果您想反射一个private
成员,您必须将您的类设为xm::DefineClass
模板的朋友。
假设我们想反射以下类及其成员
class MyClass {
public:
int myMethod(int a, int b);
int myField;
int getMyField2();
void setMyField2(int val);
private:
int myField2;
};
要注册这个类,我们首先必须在某个头文件中(在类定义之后)放置以下宏。它将特化反射系统所需的一些模板
XM_DECLARE_CLASS(MyClass);
然后在某个编译单元中(可能在*MyClass.cpp*中,但不一定是),您必须放置XM_DEFINE_CLASS
宏。这个宏隐藏了一个函数的签名。您必须编写它的主体,并且在主体内,您可以将类的属性和方法绑定到将表示反射类的Class
对象。
对于之前的定义,您应该编写类似这样的代码
XM_DEFINE_CLASS(MyClass)
{
// Bind a method with automatic name extrapolation
bindMethod(XM_MNP(myMethod));
// Bind a property from a field
bindProperty(XM_MNP(myField));
// Bind a property from get and set methods
bindProperty("myField2", &MyClass::getMyField2, &MyClass::setMyField2);
}
XM_REGISTER_TYPE(MyClass);
如果您不希望类型在程序启动时自动注册,可以省略最后一个宏。
就这么简单。模板推导机制用于提取所有类型信息。
XM_MNP (成员名称和指针) 让你避免将成员的指针和名称作为参数写入 `bindProperty`,预处理器用于将字段名称字符串化。
现在,你可以在主函数中访问`Class`对象,并像这样使用`Method`类的对象来调用方法。
MyClass myInstance;
const xm::Class& clazz = xm::getClass("MyClass");
const xm::Method& method = clazz.getMethod("myMethod");
int res = method.call(xm::ref(myInstance), 1, 2);
我将在下一段解释`xm::ref`的作用。
相反,如果您想访问一个属性,可以检索相应的`Property`对象并使用它的`getData`和`setData`方法。
MyClass myInstance;
const xm::Class& clazz = xm::getClass("MyClass");
const xm::Property& property = clazz.getProperty("myField");
property.setData(xm::ref(myInstance), 1);
您可能会想,`Method::call`和`Property::getData/Property::setData`是如何接受任何类型的参数的。实际上,这些函数只接受`Variant`。但由于`Variant`有一个模板化的构造函数,传入的参数会自动转换为`Variant`。当然,函数可以接受的参数数量是有限的,因为您不能将类型转换与可变参数列表一起使用。这个数量保存在`XM_FUNCTION_PARAM_MAX`宏中。
变体
此类型可以存储任何反射类型。它可以在两种模式下工作:作为**值** `Variant` 或作为**引用** `Variant`。
在第一种情况下,它自动管理所持有对象的内存。在第二种情况下,它只引用由他人管理的对象。
您可以使用实用方法`xm::ref()`创建引用`Variant`,就像之前的示例一样,在那里它被用作“`this`”参数来调用方法或访问属性。
`Variant`只能被转换成它们最初构建的完全相同的类型,除了类的引用`Variant`。您可以将此类`Variant`转换为基类或派生类,只要`dynamic_cast`到预期类型成功即可。否则将抛出异常。
你也可以将从预期参数类的基类或派生类构造的`Variant`传递给`Method::call`或`Property::getData`/`Property::setData`,并且将执行类型转换。
我们来看一些例子
MyReflectedClass myRelfectedObject;
// Construct a value Variant from any reflected object
xm::Variant var(myReflectedObject);
// Construct a reference Variant
xm::Variant var2 = xm::ref(myReflectedObject);
// Cast the variant back to the original type
MyReflectedClass myObj2 = var.as<MyReflectedClass>();
// Same here, but the casting is implicit through the casting operator
MyReflectedClass myObj3 = var;
// Throw an exception because of the type mismatch
int a = var2;
继承
eXtendedMirror支持继承和多重继承。假设你有一个这样的类层次结构
class MyBase1 {
// Body here
};
class MyBase2 {
// Body here
};
class MyDerived : public MyBase1, public MyBase2 {
// Body here
};
要将`MyDerived`注册为`MyBase1`和`MyBase2`的子类,您只需在派生类的`XM_DEFINE_CLASS`主体内使用`XM_BIND_BASE`或`XM_BIND_PBASE`宏。
两者之间的区别在于,`XM_BIND_PBASE`假定您的类是多态的(具有`vtable`并且可以安全地向下转换),而`XM_BIND_BASE`应该用于非多态类。
XM_DEFINE_CLASS(MyDerived)
{
XM_BIND_PBASE(MyBase1);
XM_BIND_PBASE(MyBase2);
// Member binding here
}
就是这样!
命名空间
支持命名空间。要将类注册为命名空间的一部分,只需在`XM_DECLARE_CLASS`参数中指定完整的限定名称即可。
XM_DECLARE_CLASS(myNS::MyClass);
就这么简单。
特殊方法
当您注册一个类时,系统默认假定它可以通过**公共默认构造函数**实例化,通过**公共拷贝构造函数**复制,并通过**公共析构函数**销毁。
如果情况并非如此,您可以在`XM_DEFINE_CLASS`之前使用这些自解释宏
XM_ASSUME_NON_INSTANTIABLE(MyClass); // MyClass is not instantiable (by default constructor)
XM_ASSUME_NON_DESTRUCTIBLE(MyClass); // MyClass is not destructable
XM_ASSUME_NON_COPYABLE(MyClass); // MyClass is not copyable
XM_DEFINE_CLASS(MyClass);
由于抽象类总是如此,因此提供了另一个宏来组合之前的树
XM_ASSUME_ABSTRACT(MyAbstractClass);
XM_DEFINE_CLASS(MyAbstractClass);
如果你的类没有`public`默认构造函数,但你有一个非默认的`public`构造函数,并且你希望系统能够生成此类的对象而无需更改类定义,你可以特化模板`ConstructorImpl`以手动向系统提供一个工厂函数,该函数将用于调用非默认构造函数。假设`MyClass`有一个构造函数`MyClass(int, int, const char*)`,那么你可以这样写
template<>
class ConstructorImpl<MyClass> : public Constructor {
public:
ConstructorImpl(const Class& owner) : Constructor(owner) {};
void init(Variant& var) const
{
new (&var.as<MyClass>()) MyClass(1, 2, "test");
}
};
非成员项
自由函数、常量、`enum`和`global`/`static`变量(统称为“自由项”)也受系统支持。如果您有以下代码
namespace myNS {
int myFunction(int arg1, int arg2);
const int MyConst = 4;
enum MyEnum {
Val0,
Val1,
Val2
}
int myGlobal;
}
我们可以使用编译单元中的`XM_BIND_FREE_ITEMS`宏将它们全部注册。像`XM_DEFINE_CLASS`一样,这个宏隐藏了函数签名等内容,您必须编写它的主体。
主体中使用的宏是自解释的,我不会费心去解释它们。
XM_BIND_FREE_ITEMS
{
XM_BIND_FUNCTION(myNs::myFunction);
XM_BIND_COSTANT(myNs::MyConst);
XM_BIND_ENUM(myNs::MyEnum)
.XM_ADD_ENUM_VAL(Val0)
.XM_ADD_ENUM_VAL(Val1)
.XM_ADD_ENUM_VAL(Val2);
XM_BIND_VARIABLE(myNs::myGlobal);
}
这些项目将在程序启动时自动注册,您可以在代码中像这样访问它们
const xm::Function& func = xm::getFunction("myNs::myFunction");
int res = func.call(10, 20);
const xm::Constant& constant = xm::getConstant("myNs::MyConst");
int val = constant.getValue();
const xm::Enum& enumerator = xm::getEnum("myNs::MyEnum");
int val2 = enumerator.getValue("Val2");
const xm::Variable& variable = xm::getVariable("myNs::myGlobal");
int val2 = variable.getReference();
variable.getReference().as<int>() = 5;
这是不言自明的。只需注意,`enum`被转换为普通的整数,因此`Enum::getValue()`返回一个`int`类型的`Variant`对象,而`Variable::getReference()`返回一个引用该变量的引用`Variant`对象。因为,当您使用`as()`或转换操作符转换`Variant`时,您是按引用转换的,所以您可以编写类似最后一行的代码,它实际上会设置被引用变量的值。
静态成员
没有专门的方法来反射`static`成员,因为它们基本上是命名空间内部的非成员项,命名空间与它们所属的类的名称相同。由于`Class`对象也是一个`Namespace`,因此要反射`static`成员,您只需将它们反射为非成员项并指定完整的限定名。为清晰起见,这里有一个示例头文件
class MyClass {
static int myStaticFunction(int arg1, int arg2);
static const int MyConst = 4;
enum MyEnum {
Val0,
Val1,
Val2
}
static int myStatic;
};
以及在编译单元中对应的注册宏
XM_BIND_FREE_ITEMS
{
XM_BIND_FUNCTION(MyClass::myStaticFunction);
XM_BIND_COSTANT(MyClass::MyConst);
XM_BIND_ENUM(MyClass::MyEnum)
.XM_ADD_ENUM_VAL(Val0)
.XM_ADD_ENUM_VAL(Val1)
.XM_ADD_ENUM_VAL(Val2);
XM_BIND_VARIABLE(MyClass::myStatic);
}
模板
有两种方式来反射模板:对于只有类型(即没有常量值)作为模板参数的模板,您可以使用第一种更强大的方法。
假设你有以下类模板
template<typename T1, typename T2>
class MyClass {
public:
T1 myMethod(T2 a, T2 b);
T1 myField;
T2 getMyField2();
void setMyField2(T2 val);
private:
T2 myField2;
};
这个过程与反射普通类非常相似。你首先在头文件中使用提供的正确数量为`N`的`XM_DECLARE_TEMPLATE_N`函数“声明”模板类,例如
XM_DECLARE_TEMPLATE_2(MyClass);
然后你“定义”类。然而这次,你必须将这段代码放在一个所有将注册此模板类型实例的编译单元都可以访问的头文件中。
template<typename T1, typename T2>
XM_DEFINE_CLASS(MyClass<T1, T2>)
{
// Bind a method with automatic name extrapolation
bindMethod(XM_MNP(myMethod));
// Bind a property from a field
bindProperty(XM_MNP(myField));
// Bind a property from get and set methods
bindProperty("myField2", &MyClass::getMyField2, &MyClass::setMyField2);
}
请记住,您可以特化或部分特化您的类,并且只要您保持反射过的接口的相同部分,一切都将正常。特化并改变接口并没有太大用处,所以这不应该是一个问题。
现在你可以像这样明确地注册一个从这个模板实例化的类
XM_REGISTER_TYPE(MyClass<int, float>);
这种方法的强大之处在于,你甚至不需要显式注册模板的每个可能的实例化。每当使用从该模板实例化的类型的函数、方法、属性、常量或变量被注册时,该实例化的类型也会被注册。你可以访问`CompoundClass`的`Template`对象并检索其名称。
例如,如果您想统一处理从同一模板实例化的所有容器,这可能会很有用。您还可以访问用于实例化模板的参数。
看看这个例子:
const xm::CompoundClass& clazz = xm::getCompoundClass("MyClass<int, float>");
std::cout << clazz.getTemplate().getName() << std::endl; // prints "MyClass"
std::cout << clazz.getTemplateArgs()[0].getType().getName() << std::endl; // prints "int"
const xm::CompoundClass& clazz2 = xm::getCompoundClass("MyClass<float, 10>");
std::cout << clazz.getTemplateArgs()[1].getValue().as<int>() << std::endl; // prints "10"
用值参数反射模板
如前所述,如果您的模板参数列表中包含值参数,上述方法将不起作用。为此情况提供了一种替代方法,尽管不太方便。
对于模板的每个可能的实例化,您都必须在头文件中放置一个`XM_DECLARE_CLASS`。
template<typename T, int I>
class MyTemplate2 {
// Definition here
};
XM_DECLARE_CLASS(MyTemplate2<float, 10>);
并且在编译单元中,对于模板的每个可能的实例化,您都必须放置一个`XM_DEFINE_CLASS`。
该类型被自动识别为`MyTemplate2`模板的实例化,但实际的模板参数不会被自动识别。您必须在`XM_DEFINE_CLASS`宏内部手动指定它们。
XM_DEFINE_CLASS(MyTemplate2<float, 10>)
{
// Here we add information about the actual arguments of the template instantiation in the
// reflection system
XM_ADD_TEMPL_ARG(xm::getType<float>());
XM_ADD_TEMPL_ARG(10);
// Bind other stuff
}
重载
要注册重载的方法或函数,我们必须通过明确指定`bindMethod`或`bindFunction`函数的模板参数来帮助推导机制选择我们想要的那一个。
假设我们有这个类
class MyClass {
public:
int myMethod(int, int);
int myMethod(float);
};
我们可以通过以下方式绑定它的两个方法
XM_DEFINE_CLASS(MyClass)
{
bindMethod<ClassT, int, int, int>(XM_MNP(myMethod));
bindMethod<ClassT, float>(XM_MNP(myMethod));
}
现在,你可能会想,当我们检索`Method`对象时,如何引用不同的重载。很简单,我们可以使用一个`Method`对象作为键,并通过使用实用函数模板`methodSign`来构造它。
这是一个例子。
const xm::Class& clazz = xm::getClass("MyClass");
const xm::Method& method = clazz.getMethod(xm::methodSign<MyClass, float>());
我不会多谈函数重载,因为这个过程与方法重载几乎相同:函数`bindFunction`和`functionSign`就像`bindMethod`和`MethodSign`一样,只是它们不将对类的引用作为第一个参数。
XM还提供了一种更直接的调用重载方法的方式,即通过`Variant`对象的`call()`方法。
看看这个例子:
Variant v(MyClass);
v.call("myMethod", 10, 10); // calls myMethod(int, int);
v.call("myMethod", 10.0f); // calls myMethod(float);
v.call("myMethod", 10); // fails because it looks for myMethod(int)
它根据传入的参数自动动态地解析要调用的适当重载。但是请注意,参数的类型和实参的类型必须完全匹配才能找到重载!
还有更多!
我介绍了API的关键功能,但这并非全部。如果您愿意,可以深入研究源代码,或阅读文档。
还可以进行进一步的改进,例如允许多个构造函数、允许将变体转换为兼容类型,以及实现一个与C++ `static`解析匹配的动态重载解析。但是支持您的项目所需的基本功能应该都在这里了。
希望您喜欢阅读,也许您会在您的下一个项目中发现这个库很有用。任何反馈(或贡献)都非常感谢。