65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.53/5 (11投票s)

2002 年 6 月 4 日

13分钟阅读

viewsIcon

90925

downloadIcon

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++ 框架,如 QtwxWindows、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_CLASSINFOEND_CLASSINFO。这些宏构成了描述和公开 RTTI 到 VCF 运行时基础。通过将这两个宏添加到类声明中,一旦类注册,我们立即获得以下功能:

  • 通过类名或类 ID 创建对象
  • 发现基类

BEGIN_CLASSINFO 宏接受多个参数:类类型(在本例中为 SimpleClass),一个表示类名的字符串,最好包含命名空间(如果适用)(在本例中为 "SimpleClass"),一个命名该类基类的字符串(在本例中为 VCF::Object),同样包含命名空间,最后是一个表示类唯一 ID 的字符串(在示例中,我使用了 GUIDGEN 创建了一个 GUID,然后将该值放入一个名为 SIMPLECLASS_CLASSID#define 中)。

END_CLASSINFO 宏仅接受类类型。

唯一的先决条件是,您必须有一个不带参数的默认构造函数,或者一个所有参数都有默认值的构造函数。

添加属性

要为类添加属性,您可以使用属性宏,并将它们(每个属性一个)放置在 BEGIN_CLASSINFOEND_CLASSINFO 宏之间。属性是通过某种 get 方法获取的值,并且可以(可选地)通过某种 set 方法进行设置。

对于基本类型,如 longshortcharboolString(这是 std::basic_string 类的 typedef),get 方法的形式始终为

Type <methodName>();

对于基本类型,如 longshortcharboolString(这是 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 方法的函数指针,最后是一个指示属性类型的 ValueREADONLY_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 的方法,接下来的参数是一系列逗号分隔的参数类型(例如 doubleconst bool&MyObject* 等),后面跟着方法指针,然后是一个描述参数的字符串(例如,如果参数是 doublebool,则字符串为 "db")。
  • 对于没有参数且有返回类型的方法,下一个参数是返回类型,最后一个参数是方法指针本身。
  • 对于没有参数且有返回类型的方法,下一个参数是返回类型,接下来的参数是一系列逗号分隔的参数类型(例如 doubleconst bool&MyObject* 等),后面跟着方法指针,然后是一个描述参数的字符串(例如,如果参数是 doublebool,则字符串为 "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 文件,该文件是通过解析类声明并自动输出所需信息生成的。

© . All rights reserved.