在 VCF 中使用 C++ RTTI/反射 API






2.53/5 (11投票s)
2002 年 6 月 4 日
13分钟阅读

90925

420
VCF 中 RTTI 的教程和简要说明。
关于 Visual Component Framework
Visual Component Framework 的灵感来自于 NeXTStep 的 Interface Builder、JBuilder、Visual J++ 以及 Borland 的 Delphi 和 C++ Builder 等环境的易用性。我想要一个通用的 C++ 类框架,我可以用它来快速构建应用程序(在设计 GUI 时),并且框架的核心尽可能跨平台。Visual Component Framework 是一个开源项目,所以如果您认为它可能有用,请随时获取并使用它。如果您真的喜欢冒险,可以 自愿 帮助开发它,使其变得更好,尤其是在将其集成到 VC++ 环境作为插件方面。有关 项目、提供帮助 或只是浏览 doxygen 生成的文档 的更多信息,请访问 Source Forge 上的 VCF 项目 此处,或项目的 网站。代码可从 CVS 获取(请遵循 Windows 上设置 CVS 的 此处 的操作指南),或者作为项目 文件 部分的 tar.gz 文件。
引言
本教程将介绍如何使用 Visual Component Framework 的 RTTI/反射 API。我们将学习如何为一个类添加基本的 RTTI 支持,然后深入更高级的选项。本教程将在所有 Win32 平台、Linux 2.4 或更高版本以及 Solaris 2.8 或更高版本上运行。
RTTI,即运行时类型信息,对于 VCF 这样的基于组件的系统至关重要。VCF 包含许多部分,这些部分在运行时可能并不完全可知,因此需要某种 RTTI 或类似反射的 API 来描述一个组件。其他语言如 Java、C#、ObjectPascal、Smalltalk、Objective C(仅举几例),以及其他 C++ 框架,如 Qt、wxWindows、MFC、COM(好吧,这实际上不是一个仅限 C++ 的框架,但我们姑且这么说)、VRS3D 等,也在不同程度上实现了某种 RTTI 系统。大多数语言都对从类名动态创建对象、动态属性和动态方法调用等功能提供全面支持。一些语言还支持发现类的字段(如 Java)以及发现类支持的事件。C++ 本身提供了确定给定类的基类的能力,即使用 dynamic_cast
运算符,您可以判断类 B 是否以某种方式继承自类 A。此外,typeid()
方法将返回一个 type_info
类,该类可以在运行时告诉您某个实例的类名。显然,这还不够,因此许多 C++ 框架倾向于实现某种形式的自身 RTTI。在我看来,VCF 有趣之处在于它提供的 RTTI 功能的深度,即
- 从类名创建对象
- 发现基类
- 运行时发现和动态调用属性
- 运行时发现和动态调用方法
- 运行时发现事件
- 运行时发现接口
- 运行时发现和动态调用接口方法
本文将引导您完成创建一系列使用 VCF 公开更高级 RTTI 的 C++ 类的步骤,然后创建一个简单的控制台程序来演示如何在框架中使用 RTTI。
RTTI 基础
VCF 中的 RTTI 通过一系列抽象类进行描述,并通过大量使用模板来实现。这使得它类型安全,并且不需要任何奇怪的 void 指针类型转换。为了简化,存在许多宏,允许您在类声明中指定您希望公开的 RTTI 元素,因此没有单独的构建步骤,如 IDL 编译或其他预处理器工具。这些宏基本上“收集”了 C++ 编译器在编译期间知道但会丢弃的所有信息。
每个 RTTI 元素都注册在一个名为 VCF::ClassRegistry
的中央存储库中——这是一个单例实例,在 FoundationKit 初始化时创建,在 FoundationKit 终止时销毁。一个类的注册只发生一次,如果尝试注册一个已存在的类,注册将失败。
基本的 RTTI 元素是 Class
类——它保存了在运行时描述 C++ 类所需的所有信息。一目了然,Class
类具有以下方法:
getSuperClass()
getClassName()
getID
createInstance()
getInterfaces()
getMethods()
getProperties()
getEvents()
如您所见,这些方法允许我们访问大多数重要的 C++ 类信息。
添加类名和动态创建支持
好的,让我们创建一个具有最少 RTTI 支持的 C++ 类。
#define SIMPLECLASS_CLASSID "FB685669-6D44-4ea7-8011-B513E3808002" class SimpleClass : public VCF::Object { public: BEGIN_CLASSINFO( SimpleClass, "SimpleClass", "VCF::Object", SIMPLECLASS_CLASSID ) END_CLASSINFO( SimpleClass ) SimpleClass(){} virtual ~SimpleClass(){ } };
这声明了一个名为 SimpleClass
的类,它派生自 VCF::Object
,这是整个 VCF 的基类。我们看到使用了两个宏:BEGIN_CLASSINFO
和 END_CLASSINFO
。这些宏构成了描述和公开 RTTI 到 VCF 运行时基础。通过将这两个宏添加到类声明中,一旦类注册,我们立即获得以下功能:
- 通过类名或类 ID 创建对象
- 发现基类
BEGIN_CLASSINFO
宏接受多个参数:类类型(在本例中为 SimpleClass
),一个表示类名的字符串,最好包含命名空间(如果适用)(在本例中为 "SimpleClass
"),一个命名该类基类的字符串(在本例中为 VCF::Object
),同样包含命名空间,最后是一个表示类唯一 ID 的字符串(在示例中,我使用了 GUIDGEN 创建了一个 GUID,然后将该值放入一个名为 SIMPLECLASS_CLASSID
的 #define
中)。
END_CLASSINFO
宏仅接受类类型。
唯一的先决条件是,您必须有一个不带参数的默认构造函数,或者一个所有参数都有默认值的构造函数。
添加属性
要为类添加属性,您可以使用属性宏,并将它们(每个属性一个)放置在 BEGIN_CLASSINFO
和 END_CLASSINFO
宏之间。属性是通过某种 get 方法获取的值,并且可以(可选地)通过某种 set 方法进行设置。
对于基本类型,如 long
、short
、char
、bool
和 String
(这是 std::basic_string
类的 typedef
),get 方法的形式始终为
Type <methodName>();
对于基本类型,如 long
、short
、char
、bool
和 String
(这是 std::basic_string
类的 typedef
),set 方法的形式始终为
void <methodName>( const Type& );
对于 Object
类型(即任何派生自 VCF::Object
的类),get 和 set 方法具有此形式:
ObjectType* <getMethodName>();
void <setMethodName>( ObjectType* );
在添加属性时,您可以使用多种不同的宏,具体取决于属性类型以及您是否希望属性是只读的。在考虑类型时,请注意,有专门的属性宏用于处理枚举值,以便系统能够显示 enum
值的符号名称,而不是仅仅显示整数值。以下宏适用于基本类型、Object
派生属性以及 enum
类型的属性:
PROPERTY
- 读/写属性READONLY_PROPERTY
- 只读属性OBJECT_PROPERTY
- 属性是Object
派生且是读/写的READONLY_OBJECT_PROPERTY
- 属性是Object
派生且是只读的ENUM_PROPERTY
- 属性是enum
且是读/写的LABELED_ENUM_PROPERTY
- 属性是enum
且是读/写的,并且有一组字符串标签定义了enum
的符号名称READONLY_ENUM_PROPERTY
- 属性是enum
且是只读的READONLY_LABELED_ENUM_PROPERTY
- 属性是enum
且是只读的,并且有一组字符串标签定义了enum
的符号名称
因此,为了实践,让我们看看我们的下一个示例类:
#define LITTLELESSSIMPLECLASS_CLASSID "F027BCD9-B8BF-4e52-A6E0-0EA3CC080B90" class LittleLessSimpleClass : public SimpleClass { public: BEGIN_CLASSINFO( LittleLessSimpleClass, "LittleLessSimpleClass", "SimpleClass", LITTLELESSSIMPLECLASS_CLASSID ) PROPERTY( long, "size", LittleLessSimpleClass::getSize, LittleLessSimpleClass::setSize, pdLong ) PROPERTY( bool, "cool", LittleLessSimpleClass::getCool, LittleLessSimpleClass::setCool, pdBool ) PROPERTY( char, "char", LittleLessSimpleClass::getChar, LittleLessSimpleClass::setChar, pdChar ) PROPERTY( String, "name", LittleLessSimpleClass::getName, LittleLessSimpleClass::setName, pdString ) READONLY_OBJECT_PROPERTY( Object, "owner", LittleLessSimpleClass::owner ) END_CLASSINFO( LittleLessSimpleClass ) LittleLessSimpleClass():m_size(0),m_cool(true), m_char('f'),m_name("Howdy"),m_owner(NULL){}; virtual ~LittleLessSimpleClass(){} long getSize() { return m_size; } void setSize( const long& size ) { m_size = size; } bool getCool() { return m_cool; } void setCool( const bool& cool ) { m_cool = cool; } char getChar() { return m_char; } void setChar( const char& ch ) { m_char = ch; } String getName() { return m_name; } void setName( const String& s ) { m_name = s; } Object* owner() { return m_owner; } protected: long m_size; bool m_cool; char m_char; String m_name; Object* m_owner; };
好的,这稍微复杂一些,因为我们为 LittleLessSimpleClass
公开了多个属性。我们公开了 5 个属性,其中 4 个是读/写的,1 个是只读的。对于基本类型,PROPERTY
宏接受类型、属性名称字符串、get 方法的函数指针、set 方法的函数指针,最后是一个指示属性类型的 Value
。READONLY_PROPERTY
宏接受与之前相同的参数,除了 set 方法指针。READONLY_OBJECT_PROPERTY
宏接受对象类型、属性名称字符串和 get 方法指针。OBJECT_PROPERTY
宏接受相同的参数,但您还需要传入一个 set 方法指针供其使用。
继承自 LittleLessSimpleClass
并通过至少使用 BEGIN_CLASSINFO
/ END_CLASSINFO
宏公开 RTTI 的类将自动获得与其基类相同的属性集。对于方法和事件也是如此。
#define FooBarBazifier_CLASSID "1658339E-5132-4007-A9B4-0DE58F89AEBE" class FooBarBazifier : public Object { public: BEGIN_CLASSINFO( FooBarBazifier, "FooBarBazifier", "VCF::Object", FooBarBazifier_CLASSID ) METHOD_VOID( "printMe", FooBarBazifier, printMe, &FooBarBazifier::printMe ) METHOD_2VOID( "add", FooBarBazifier, add, const double&, const double&, &FooBarBazifier::add, "dd" ) METHOD_RETURN( "whereAmI", FooBarBazifier, whereAmI, String, &FooBarBazifier::whereAmI ) METHOD_2RETURN( "addAndReturn", FooBarBazifier, addAndReturn, int, const double&, const double&, &FooBarBazifier::addAndReturn, "dd" ) END_CLASSINFO( FooBarBazifier ) FooBarBazifier() { } virtual ~FooBarBazifier() { } void printMe() { System::print( "print me!\n" ); } String whereAmI() { return "Where am i ?"; } void add( const double& d1, const double& d2 ) { System::print( "%.4f + %.4f = %.4f\n", d1, d2, d1+d2 ); } int addAndReturn( const double& d1, const double& d2 ) { return (int)(d1 + d2); } };
从上面的示例可以看出,方法宏的格式如下:
METHOD_<argument number><return type>
其中参数数量为 1 到 6(如果方法不接受参数则为 0),返回类型为 VOID
表示方法没有返回值,RETURN
表示方法有返回值。因此,宏 METHOD_VOID
用于没有参数且返回类型为 void
的方法。像 METHOD_3RETURN
这样的宏用于有 3 个参数并有返回值的类。
添加方法
我们的下一个类声明将演示如何使用 VCF RTTI 宏公开类的各种方法。与属性一样,您将这些宏放在 BEGIN_CLASSINFO
/ END_CLASSINFO
宏之间,并为每个您想公开的成员方法使用一个方法宏。
方法通过 VCF::Method
类进行封装,该类基本上将一个函数指针与类的成员方法挂接,并附带有关该方法的一些信息,例如它接受多少参数、方法名称等。
任何方法都可以通过 RTTI 公开,但(目前)方法参数数量有限制:当前方法最多可以有 6 个参数。如果您愿意,可以通过编写更多宏来更改此设置——请参阅 vcf/include/ClassInfo.h 文件以了解其工作原理。
方法参数类型在一个特殊字符串格式中描述,其中每个字符指定一个特定的参数类型:
代码 | 原始类型 |
"i" | int |
"+i" | unsigned int |
"l" | long |
"+l" | unsigned long |
"h" | short |
"+h" | unsigned short |
"c" | char |
"+c" | unsigned char |
"d" | double |
"f" | float |
"b" | bool |
"s" | 字符串 |
"o" | Object* |
"e" | Enum* |
因此,字符串 "h+lod" 将表示一组参数,包括一个 short
、一个 unsigned long
、一个 Object*
和一个 double
。
方法宏的参数如下:
- 前三个参数是表示方法名称的字符串、类类型和方法 ID(应与方法名称相同,但去掉字符串引号,因此名为 "
bar
" 的方法 ID 为bar
)。 - 对于没有参数且返回
void
/无返回的方法,最后一个参数是方法指针本身。 - 对于有 1 个或更多参数且返回
void
的方法,接下来的参数是一系列逗号分隔的参数类型(例如double
、const bool&
或MyObject*
等),后面跟着方法指针,然后是一个描述参数的字符串(例如,如果参数是double
、bool
,则字符串为 "db")。 - 对于没有参数且有返回类型的方法,下一个参数是返回类型,最后一个参数是方法指针本身。
- 对于没有参数且有返回类型的方法,下一个参数是返回类型,接下来的参数是一系列逗号分隔的参数类型(例如
double
、const bool&
或MyObject*
等),后面跟着方法指针,然后是一个描述参数的字符串(例如,如果参数是double
、bool
,则字符串为 "db")。
注册类
到目前为止,我们已经学习了如何为类声明添加各种 RTTI 部分,但这在您将类信息注册到 VCF::ClassRegistry
之前没有任何作用。同样,存在一系列用于实现此功能的方法,但如果您使用了 RTTI 宏,那么您可以一步完成所有这些工作(每个类)。
REGISTER_CLASSINFO( LittleLessSimpleClass )
通过使用 REGISTER_CLASSINFO
宏,您可以注册您的类以及类公开的任何属性、方法等,只需将类类型传递给 REGISTER_CLASSINFO
宏即可。这将在 VCF::ClassRegistry
中为指定的类类型放置一个 VCF::Class
对象的 **单个** 实例,无论之后创建多少该实际类类型的实例。此外,以下代码
REGISTER_CLASSINFO( LittleLessSimpleClass ) REGISTER_CLASSINFO( LittleLessSimpleClass ) REGISTER_CLASSINFO( LittleLessSimpleClass )
将 **仅** 注册类类型 LittleLessSimpleClass
**一次**,重复的注册尝试将被忽略。
动态创建类
一旦类被注册,通过类名创建实例就非常简单,如下面的代码所示:
Object* simpleClassInstance = NULL;
ClassRegistry* classRegistry = ClassRegistry::getClassRegistry();
classRegistry->createNewInstance( "SimpleClass", &simpleClassInstance );
我们通过调用静态方法 VCF::ClassRegistry::getClassRegistry()
来获取类注册表,该方法返回 VCF::ClassRegistry
单例。
我们通过调用 VCF::ClassRegistry::createNewInstance()
来创建实例,传入要创建实例的类名,以及一个将分配给新实例的指针的引用。如果方法失败,将抛出 VCF::CantCreateObjectException
。
或者,如果您可以访问 VCF::Class
,那么您可以直接从此创建实例,如下所示:
Object* anObject = ....//we have an object from somewhere...
Object* newInstance = NULL;
Class* clazz = anObject->getClass();
clazz->createInstance( &newInstance );
瞧! 我们创建了由 clazz
变量表示的类的全新实例。
查询 RTTI 信息
检索 RTTI 信息非常容易。VCF::Object
有一个名为 getClass()
的方法,它返回一个 VCF::Class
实例,该实例代表该类类型的 RTTI 信息。以下方法演示了您如何在运行时动态获取有关类的详细信息。
void reportRTTIData( Object* object ) { Class* clazz = object->getClass(); if ( NULL != clazz ) { System::print( "\n\nClass retreived, information: \n" ); System::print( "Class name:\t%s\nSuper Class name:\t%s\n", clazz->getClassName().c_str(), clazz->getSuperClass()->getClassName().c_str() ); System::print( "Number of properties known to RTTI: %d\n", clazz->getPropertyCount() ); Enumerator<Property*>* properties = clazz->getProperties(); while ( properties->hasMoreElements() ) { Property* property = properties->nextElement(); VariantData data = *property->get(); System::print( "\tProperty \"%s\", is read only: %s, value: %s\n", property->getDisplayName().c_str(), property->isReadOnly() ? "true" : "false", data.toString().c_str() ); } System::print( "Number of interfaces known to RTTI: %d\n", clazz->getInterfaceCount() ); System::print( "Number of methods known to RTTI: %d\n", clazz->getMethodCount() ); Enumerator<Method*>* methods = clazz->getMethods(); while ( methods->hasMoreElements() ) { Method* method = methods->nextElement(); System::print( "\tMethod name: \"%s\", number of arguments: %d\n", method->getName().c_str(), method->getArgCount() ); } } }
上面示例中使用的 Enumerator 类 **不是** 自制的集合类,而是非常薄的包装器,用于隐藏实现特定的集合类(如 list、vector、map 等),当您只需要能够迭代一系列项的能力时。
动态调用方法
动态调用方法同样非常容易。可以从 VCF::Class
中检索方法,然后调用 VCF::Method::invoke()
方法。例如:
Object* object = NULL; ClassRegistry* classRegistry = ClassRegistry::getClassRegistry(); classRegistry->createNewInstance( "FooBarBazifier", &object ); Class* clazz = object->getClass(); Method* addMethod = clazz->getMethod("add"); if ( NULL != addMethod ) { VariantData* args[] = {new VariantData(), new VariantData()}; *(args[0]) = 23.909; *(args[1]) = 1220.3490; addMethod->invoke( args ); delete args[0]; delete args[1]; }
这演示了创建 FooBarBazifier
类型的类(我们之前处理过,见上文),获取其 Class
,然后查询名为 "add
" 的方法。如果 Class
具有此方法,它将返回指向该方法的指针,否则返回 NULL
。要调用该方法,我们必须组装一个 VCF::VariantData
实例数组。VCF::VariantData
允许存储任何数据类型,并具有赋值和转换运算符,使其易于使用(它类似于 COM 的 Variant
结构)。然后我们将参数数组传递给 VCF::Method::invoke
方法,该方法简单地反过来调用其函数指针。
优点和缺点
我想我已经展示了一个相当强大的 RTTI 系统,本文只是对其进行了初步介绍。它提供了与 Java 等语言或 .NET 等框架相当的功能集。主要缺点(IMO)是它要求手动向类定义添加宏,这可能会很繁琐。我已经试验过一个单独的工具,可以用来创建一个独立的 .h/.cpp 文件,该文件是通过解析类声明并自动输出所需信息生成的。