`
jiagou
  • 浏览: 2525341 次
文章分类
社区版块
存档分类
最新评论

RTTI 运行时类型识别

 
阅读更多

c++ primer3th

RTTI运行时类型识别)允许“用指向基类的指针或引用来操纵对象”的程序能够获取到“这些指针或引用所指对象“的实际派生类型。在c++中,为了支持RTTI提供了两个操作符:

1dynamic_cast 操作符,它允许在运行时刻进行类型转换,从而使程序能够在一个类层次结构中安全地转换类型,把基类指针转换成派生类指针,或把指向基类的左值转换成派生类的引用,当然只有在保证转换能够成功的情况下才可以。

2typeid 操作符,它指出指针或引用指向的对象的实际派生类型。

但是,对于要获得的派生类类型的信息,dynamic_cast typeid 操作符的操作数的类型必须是带有一个或多个虚拟函数的类类型。即,对于带有虚拟函数的类而言,RTTI 操作符是运行时刻的事件,而对于其他类而言,它只是编译时刻的事件。在本节,我们将更详细地了解这两个操作符所提供的支持。

在实现某些应用程序(比如调试器或数据库程序)时,RTTI的使用是很有必要的。在这些应用程序中,只有在运行时刻通过“检查与对象的类类型一起存储的RTTI 信息“,我们才能知道对象的类型。但是,我们应该尽量减少使用RTTI而尽可能多地使用C++的静态类型系统(即编译时刻类型检查),因为它是更加安全有效的。

dynamic_cast 操作符

dynamic_cast 操作符可以用来把一个类类型对象的指针转换成同一类层次结构中的其他类的指针,同时也可以用它把一个类类型对象的左值转换成同一类层次结构中其他类的引用。与C++支持的其他强制转换不同的是,dynamic_cast 是在运行时刻执行的。如果指针或左值操作数不能被转换成目标类型,则dynamic_cast 将失败。如果针对指针类型的dynamic_cast失败,则dynamic_cast 的结果是0如果针对引用类型的dynamic_cast 失败,则dynamic_cast会抛出一个异常。在后面,我们会给出失败的dynamic_cast 的示例。

在进一步详细了解dynamic_cast 的行为之前,我们先来了解为什么在C++程序中用户需要使用dynamic_cast 假设我们的程序用类库来表示公司中不同的雇员。这个层次结构中的类都支持某些成员函数,以计算公司的薪金,例如:

class employee {

public:

virtual int salary();

};

class manager : public employee {

public:

int salary();

};

class programmer : public employee {

public:

int salary();

};

void company::payroll( employee *pe ) {

// 使用 pe->salary()

}

我们的公司有不同类型的雇员。company 成员函数payroll()的参数是指向employee雇员)类的指针,它可能指向manager经理)类,也可能指向programmer程序员)类。因为payroll()调用虚拟函数salary()所以,根据pe指向的雇员的类型,它分别调用managerprogramer 类对应的函数。

假设,employee 类不再能满足我们的需要,我们想要修改它。希望增加一个名为bonus()的成员函数,在计算公司的薪金时,能与成员函数salary()一起被使用。则可以通过向employee层次结构中的类增加一个虚拟成员函数来实现。例如:

class employee {

public:

virtual int salary();

virtual int bonus();

};

class manager : public employee {

public:

int salary();

};

class programmer : public employee {

public:

int salary();

int bonus();

};

void company::payroll( employee *pe )

{

// 使用 pe->salary() pe->bonus()

}

如果payroll()的参数pe 指向一个manager 类型的对象,则调用其类employee 中定义的虚拟函数bonus() 因为manager 类型的对象没有改写employee 类中定义的虚拟函数bonus()如果payroll()的参数pe 指向一个programmer 类型的对象,则调用在programmer 类中定义的虚拟成员函数bonus()

为类层次结构增加虚拟函数时,我们必需重新编译类层次结构中的所有类成员函数。如果我们能够访问类employeemanager programmer 的成员函数的源代码,那么,就可以增加虚拟函数bonus()但这样做并不总是可能的。如果前面的类层次结构是由第三方库提供商提供的,那么,该提供商可能只提供库中定义类接口的头文件以及库的目标文件。但他们可能不会提供类成员函数的源代码。在这种情况下,重新编译该层次结构下的类成员函数是不可能的。

如果我们希望扩展这个类库,则不能增加虚拟成员函数。但我们可能仍然希望增加功能。在这种情况下,就必需使用dynamic_castdynamic_cast操作符可用来获得派生类的指针,以便使用派生类的某些细节,这些细节没有其他办法能够得到。例如,假设我们通过向programmer 类增加bonus()成员函数来扩展这个库。我们可以在programmer 类定义(在头文件中给出)中增加这个成员函数的声明,并在我们自己的程序文本文件中定义这个新的成员用数:

class employee {

public:

virtual int salary();

};

class manager : public employee {

public:

int salary();

};

class programmer : public employee {

public:

int salary();

int bonus();

};

记住,函数payroll()接收一个指向基类employee 的指针作为参数。我们可以用dynamic_cast 操作符获得派生类programmer 的指针并用这个指针,调用成员函数bonus()如下所示:

void company::payroll( employee *pe )

{

programmer *pm = dynamic_cast< programmer* >( pe );

// 如果 pe 指向 programmer 类型的一个对象,则 dynamic_cast 成功

// 并且 pm 指向 programmer 对象的开始

if ( pm ) {

// pm 调用 programmer::bonus()

}

// 如果 pe 不是指向 programmmer 类型的一个对象,则 dynamic_cast 失败

// 并且 pm 的值为 0

else {

// 使用 employee 的成员函数

}

}

dynamic_cast 转换

dynamic_cast< programmer* >( pe )

把它的操作数pe 转换成programmer*型。如果pe 指向programmer 类型的对象,则该强制转换成功。否则,转换失败,dynamic_cast 的结果为0所以dynamic_cast 操作符一次执行两个操作。它检验所请求的转换是否真的有效,只有在有效时,它才会执行转换,而检验过程发生在运行时刻。dynamic_cast 比其他C++转换操作要安全,因为其他转换不会检验转换是否真正能被执行。

在上个例子中,如果在运行时刻pe 实际上指向一个programmer 对象,则dynamic_cast成功,并且pm 被初始化,指向一个programmer 对象。否则,dynamic_cast 操作的结果是0pm 被初始化为0通过检查pm 的值,函数company::payroll()知道pe 何时指向一个programmer 对象。然后,它可以用成员函数programmr::Bonus()计算程序员的薪金。如果同为pc 指向一个manager 对象,而导致dynamic_cast 失败,则使用一般的薪金计算,不使用新的成员函数programmer::bonus()

dynamic_cast 被用来执行从基类指针到派生类指针的安全转换,它常常被称为安全的向下转换当我们必须使用派生类的特性,而该特性又没有出现在基类中时,我们常常使用dynamic_cast用指向基类类型的指针来操纵派生类类型的对象,通常通过虚拟函数自动处理。但是,在某些情形下,使用虚拟函数是不可能的。dynamic_cast 为这些情形提供了替代的机制,但是这种机制比虚拟成员函数更易出错,应该小心使用。

一种可能的错误是,在测试dynamic_cast 的结果是否为0之前就使用它。如果这样做了,则与之相应的结果不见得是正确的,它未必指向一个类对象。例如:

void company::payroll( employee *pe ) {

programmer *pm = dynamic_cast< programmer* >( pe );

// 潜在的错误: 在测试 pm 的值之前使用 pm

static int variablePay = 0;

variablePay += pm->bonus();

// ...

}

在使用结果指针之前,我们必须通过测试dynamic_cast 操作符的结果来检验转换是否成功。company::payroll()函数的一个较好的定义如下:

void company::payroll( employee *pe )

{

// dynamic_cast 和测试在同一条件表达式中

if ( programmer *pm = dynamic_cast< programmer* >( pe ) ) {

// 使用 pm 调用 programmer::bonus()

}

else {

// 使用 employee 的成员函数

}

}

dynamic_cast 操作的结果被用来初始化if 语句条件表达式中的变量pm这也是必要的,因为条件中的声明也会产生值。如果pm 不是0也就是说,如果因为指针pe 实际指向一个programmer 对象,所以dynamic_cast 成功,则执行if 语句的true 分支。否则,条件中的声明产生结果0并执行else 分支。因为dynamic_cast 操作和其结果的测试出现在同一语句中,所以不可能在dynamic_cast 和测试之间错误地插人代码,从而不可能在测试之前使用pm

在上个例子中,dynamic_cast 操作把一个基类指针转换成派生类指针。dynamic_cast 也可以用来把一个基类类型的左值转换成派生类类型的引用。这种dynamic_cast 的语法如下:

dynamic_cast< Type& >( lval )

这里的Type&是转换的目标类型,而lval 是基类类型的左值。只要Ival 指向的对象的类型“有一个基类或派生类是Type 类型“,那么dynamic_cast 操作就会把操作数lval 转换成期望的Type&因为不存在空引用,所以不可能通过比较dynamic_cast 的结果(dynamic_cast的结果引用)是否为0来检验dynamic_cast 是否成功。如果上个例子想使用引用而不是指针,则条件:

if ( programmer *pm = dynamic_cast< programmer* >( pe ) )

就不能被改写为:

if ( programmer &pm = dynamic_cast< programmer& >( pe ) )

dynamic_cast 被用来转换引用类型时,它会以一种不同的方式报告错误情况。如果一个引用的dynamic_cast 失败,则会抛出一个异常。

那么,为了使用引用类型的dynamic_cast上个例子必须被重写如下:

#include <type_info>

void company::payroll( employee &re )

{

try {

programmer &rm = dynamic_cast< programmer & >( re );

// rm 调用 programmer::bonus()

}

catch ( std::bad_cast ) {

// 使用 employee 的成员函数

}

}

如果一个引用的dynamic_cast 失败,则它会抛出一个bad_cast 类型的异常。类类型bad_cast 被定义在C++标准库中。为了像这个例子那样在程序中引用该类型,我们必须包含头文件<type_info>

什么时候应该使用针对引用的dynamic_cast而不是针对指针的?这实在是程序员的选择。对引用的dynamic_cast 不可能忽略失败的转换并在没有测试其结果前使用它而指针是有可能的。但是,使用异常给程序增加了相应的运行开销,所以有些程序员可能更喜欢使用指针的dynamic_cast

typeid 操作符

RTTI 提供的第二个操作符是typeid 操作符,它在程序中可用于获取一个表达式的类型。如果表达式是一个类类型,并且含有一个或多个虚拟成员函数,则答案会不同于表达式本身的类型。例如,如果表达式是一个基类的引用,则typeid 会指出底层对象的派生类类型。例如:

#include <type_info>

programmer pobj;

employee &re = pobj;

// 我们将在下面关于 type_info 的小节中

// 看到 name()

// name() 返回 C 风格字符串: "programmer"

cout << typeid( re ).name() << endl;

typeid 操作符的操作数re的类型是employee但是,因为re 是带有虚拟函数的类类型的引用,所以typeid 操作符的结果指出,底层对象的类型是programmer类型(而不是操作数的类型employee)。使用typeid 操作符时,程序文本文件必须包含C++标准库中定义的头文件<type_info>

typeid 操作符可用来做什么?它用在高级的系统程序设计开发中,例如,设计构造调试器,或用来处理从数据库获取到的永久性对象。在这样的系统中,在一个调试会话中,或者向一个数据库存贮或获取对象期间,当程序通过基类指针或引用操纵一个对象时,程序需要找到被操纵的对象的实际类型,以便正确地列出对象的属性。为了找到对象的实际类型,我们可以使用typeid

typeid 操作符必须与表达式或类型名一起使用。例如,内置类型的表达式和常量可以被用作typeid 的操作数。当操作数不是类类型时,typeid 操作符会指出操作数的类型:

int iobj;

cout << typeid( iobj ).name() << endl; // 打印: int

cout << typeid( 8.16 ).name() << endl; // 打印: double

typeid 操作符的操作数是类类型,但不是带有虚拟函数的类类型时,typeid 操作符会指出操作数的类型,而不是底层对象的类型:

class Base { /* 没有虚拟函数 */ };

class Derived : public Base { /* 没有虚拟函数 */ };

Derived dobj;

Base *pb = &dobj;

cout << typeid( *pb ).name() << endl; // 打印: Base

typeid 操作符的操作数是Base类型的,即表达式*pb 的类型。因为Base不是一个带有虚拟函数的类类型,所以typeid 的结果指出,表达式的类型是Base尽管pb 指向的底层对象的类型是Derived

可以对typeid 的结果进行比较,例如:

#include <type_info>

employee *pe = new manager;

employee& re = *pe;

if ( typeid( pe ) == typeid( employee* ) ) // true

// do something

/*

if ( typeid( pe ) == typeid( manager* ) ) // false

if ( typeid( pe ) == typeid( employee ) ) // false

if ( typeid( pe ) == typeid( manager ) ) // false

*/

if 语句的条件子句比较“在一个表达式上应用typeid 操作符的结果“和”用在类型名操作数上的typeid 操作符的结果“。注意比较:

typeid( pe ) == typeid( employee* )

的结果为true这使得习惯写:

// 调用虚拟函数

pe->salary();

的用户有些吃惊,它导致调用manager 派生类的函数salary()typeid(pe)与虚拟函数调用机制不同。这是因为操作数pe 是一个指针,而不是一个类类型。为了要获取到派生类类型,typeid 的操作数必须是一个类类型(带有虚拟函数)。表达式typeid(pe)指出pe 的类型,即指向employee 的指针。它与表达式typeid(employee*)相等,而其他比较的结果都是false

当表达式*pe 被用在typeid 上时,结果指出pe 指向的底层对象的类型:

typeid( *pe ) == typeid( manager ) // true

typeid( *pe ) == typeid( employee ) // false

在这两个比较中,因为*pe 是一个类类型的表达式,该类带有虚拟函数,所以typeid 的结果指出操作数所指的底层对象的类型,即manager

typeid 操作符也可以被用在引用上,例如:

typeid( re ) == typeid( manager ) // true

typeid( re ) == typeid( employee ) // false

typeid( &re ) == typeid( employee* ) // true

typeid( &re ) == typeid( manager* ) // false

在前两个比较中,操作数re 是带有虚拟函数的类类型,因此typeid 操作数的结果指出re指向的底层对象的类型。在后两个比较中,操作数&re 是一个类型指针。因此typeid 操作符的结果指出操作数的类型,即employee*

typeid 操作符实际上返回一个类型为type_info 的类对象。type_info 类类型被定义在头文件<type_info>中,它的类接口描述了我们可以对typeid 操作符的结果做什么操作。我们将在下一小节看到这个接口。

type_info

type_info 类的确切定义是与编译器实现相关的,但是这个类的某些特性对每个C++程序却都是相同的:

class type_info {

// 依赖于编译器的实现

private:

type_info( const type_info& );

type_info& operator= ( const type_info& );

public:

virtual ~type_info();

int operator==( const type_info& ) const;

int operator!=( const type_info& ) const;

const char * name() const;

};

因为type_info 类的拷贝构造函数和拷贝赋值操作符都是私有成员,所以用户不能在自己的程序中定义type_info 对象,例如:

#include <typeinfo>

type_info t1; // 错误: 没有缺省构造函数

// 错误: 拷贝构造函数是 private

type_info t2 ( typeid( unsigned int ) );

在程序中创建type_info 对象的惟一途径是使用typeid 操作符

该类还有重载的比较操作符。这些操作符允许比较两个type_info 对象,因此允许比较“用typeid 操作符获得的结果“,如上节所示:

typeid( re ) == typeid( manager ) // true

typeid( *pe ) != typeid( employee ) // false

函数name()返回一个C 风格字符串,它是type_info 对象所表示的类型的名字。该函数可以被用在我们的程序中,如下所示:

#include <typeinfo>

int main() {

employee *pe = new manager;

// 输出: "manager"

cout << typeid( *pe ).name() << endl;

}

类型名是惟一保证被所有C++编译器实现提供的信息,可通过type_info 成员函数name()获得。正如在本节开始提到的,对RTTI 的支持是与编译器实现相关的,而且,某些编译器可能为类type_info 提供了其他成员函数,而没有在上面列出来。你应该查询编译器手册来找到确切的RTTI 支持。可能提供了哪些额外的支持?基本上,编译器为一个类型提供的任何可能的信息都可以被加进来,例如:

1类成员函数清单。

2内存中该类类型对象的布局是什么样的,即,成员和基类子对象是怎样被映射的。

编译器用来扩展RTTI 支持的一种常见技术是,为从type_info 派生的类类型增加额外的信息,因为type_info 类含有一个虚拟析构函数,所以dynamic_cast 操作符可以被用来判断是否有可用的特殊类型的RTTI 扩展支持。例如,我们假设一个编译器通过一个名为extended_type_info 的类为RTTI 提供额外的支持。extended_type_info 是一个从type_info 派生的类。通过使用dynamic_cast一个程序可以发现“typeid 操作符返回的type_info 对象“是否为extended_type_info 类型,在程序中是否可以使用额外的RTTI 支持,如下:

#include <typeinfo>

// typeinfo 头文件包含 extended_type_info 的定义

typedef extended_type_info eti;

void func( employee* p )

{

// type_info* extended_type_info* 向下转换

if ( eti *eti_p = dynamic_cast<eti *>( &typeid( *p ) ) )

{

// 如果 dynamic_cast 成功

// 通过 eti_p 使用 extended_type_info 信息

}

else

{

// 如果 dynamic_cast 失败

// 使用标准 type_info 信息

}

}

如果dynamic_cast 成功,则typeid 操作符返回一个extended_type_info 类型的对象,意味着该编译器提供了额外的RTTI 支持,可供程序使用。如果dynamic_cast 失败,则只有基本的RTTI 支持可以被程序使用。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics