C++:60---特殊工具与技术之(运行时类型识别RTTI:dynamic_cast运算符、typeid运算符、type_info类)

  • 运行时类型识别(RTTI)的功能由两个运算符实现:
    • typeid运算符:用于返回表达式的类型
    • dynamic_cast运算符:用于将基类的指针或引用安全地转换成派生类的指针或引用
  • 当我们将这两个运算符用于某种类型的指针或引用,并且该类含有虚函数时,运算符将使用指针或引用所绑定对象的动态类型
  • 这两个运算符适用于以下的情况:
    • 我们想使用基类对象的指针或引用执行某个派生类操作,并且该操作不是虚函数
    • 一般来说,只要有可能我们应该尽量使用虚函数。当操作被定义为虚函数时,编译器将根据对象的动态类型自动地选择正确的函数版本。然而,并非任何时候都能定义一个虚函数,假设我们无法使用虚函数,则可以使用一个RTTI运算符
  • 另一方面,与虚函数相比,使用RTTI运算符蕴含着更多潜在的风险:程序员必须清楚地知道转换的目标类型并且必须检查类型转换是否被成功执行

一、dynamic_cast运算符

  • dynamic_cast运算符的使用形式如下所示:
    • type:必须是一个类类型,并且通常情况下该类型应该含有虚函数
    • e:
      • 在第一种形式下:e必须是一个有效的指针
      • 在第二种形式下:e必须是一个左值
      • 在第三种形式下:e不能使左值

  • 在上面的所有形式中,e的类型必须符合以下三个条件中的任意一个:
    • e的类型是目标type的公有派生类
    • e的类型是目标type的公有基类
    • e的类型就是目标type的类型
  • 出错时的返回值:
    • 如果一条dynamic_cast语句的转换目标是指针类型且失败了,则结果为0
    • 如果一条dynamic_cast语句的转换目标是引用类型且失败了,则dynamic_cast运算符抛出一个bad_cast异常

指针类型的dynamic_cast

  • 假设Base含有虚函数,且Derived是Base的公有派生类
  • 如果有一个指向Base的指针bp,则我们可以在运行时将它转换成指向Derived的指针。代码如下:
class Base {
public:
    virtual void foo() {} //必须含有虚函数,否则不能执行dynamic_cast
};

class Derived :public Base {};

int main()
{
    Base *bp = new Base;

    //成功返回Derived指针,失败返回0
    if (Derived *dp = dynamic_cast<Derived*>(bp)) 
    {
	    //使用dp指向的Derived对象
    }
    else 
    {
		//使用bp指向的Base对象
    }

    return 0;
}
  • 如果bp成功指向Derived对象,则上述的类型转换初始化dp并令其指向bp所指的Derived对象。此时:
    • if语句内部使用Derived操作的代码是安全的
    • 否则,类型转换的结果为0,dp为0意味着if语句的条件失败,此时else子句指向相应的Base操

  • 值得注意的是,我们在条件部分定义了dp,这样做的好处是:
    • 可以在一个操作中同时完成类型转换和条件检查两项任务
    • 而且,指针dp在if语句外部是不可访问的。一旦转换失败,即使后续的代码忘了做相应判断,也不会接触到这个未绑定的指针,从而程序时安全的

引用类型的dynamic_cast

  • 引用类型的dynamic_cast与指针类型的dynamic_cast使用起来类似,只是两者在出错时的返回值不同:
    • 引用类型的dynamic_cast在出错时会抛出异常
    • 指针类型的dynamic_cast在出错时返回0
  • 当对引用的类型转换失败时,程序抛出一个名为std::bad_cast的异常,该异常定义在typeinfo头文件中
  • 例如:
class Base {
public:
    virtual void foo() {} //必须含有虚函数,否则不能执行dynamic_cast
};

class Derived :public Base {};

void f(const Base &b)
{
    try {
        //如果出错,将抛出bad_cast异常
        const Derived &d = dynamic_cast<const Derived&>(b);
    }
    catch (bad_cast) {
		
    }
}

二、typeid运算符

  • 为RTTI提供的第二个运算符是typeid运算符,它返回表达式所属对象的类型
  • typeid运算符的格式是typeid(e),其中e可以是任意表达式或类型的名字
  • typeid操作的返回值是一个常量对象的引用,该对象的类型是标准库类型type_info或type_info的公有派生类型
  • typeid运算符的一些注意事项:
    • typeid运算符可以作用于任意类型的表达式。和往常一样,顶层const被忽略
    • 如果表达式是一个引用,则typeid返回该引用所引对象的类型
    • 当typeid作用域数组或函数时,并不会执行向指针的标准类型转换。也就是说,如果我们对数组a执行typeid(a),则所得的结果是数组类型而非指针类型
  • 解析动态类型与静态类型(重要):
    • 当运算对象不属于类类型或者是一个不包含任何虚函数的类时,typeid运算符指示的是运算对象的静态类型
    • 而当运算对象是定义了至少一个虚函数的类的左值时,typeid的结果直到运行时才会求得

使用typeid运算符

  • 通常情况下,我们使用typeid比较两条表达式的类型是否相同,或者比较一条表达式的类型是否与指定类型相同
  • 演示案例:
class Base {
public:
    virtual void foo() {}
};

class Derived :public Base {};

int main()
{
    Derived *dp = new Derived;
    Base *bp = dp;

    //运行时比较,如果bp与dp指向于同一类型的对象才执行if
    if (typeid(*bp) == typeid(*dp)) {
		
    }
    //运行时比较,当bp指向于Derived类型对象才执行
    if (typeid(*bp) == typeid(Derived)) {
		
    }
    return 0;
}
  • 为什么基类要设计含有虚函数:
    • 因为类含有虚函数之后,typeid的运算将在运行时解析指针所指的对象(运行时解析)
    • 如果类不含虚函数,那么指针的类型将在编译期间都会被确定下来(编译时解析),因此如果Base不含虚函数,那么上面两个if将都返回false
  • 注意:typeid应用是是对象而不是指针,因此下面代码中的if都将返回false:
Derived *dp = new Derived;
Base *bp = dp;

//下面的if将返回false,因为bp的类型是指向Base的指针,dp是指向Derived的指针
if (typeid(bp) == typeid(dp)) {
    std::cout << "1" << std::endl;
}
//下面的if将返回false,因为bp的类型是指向Base的指针
if (typeid(bp) == typeid(Derived)) {
    std::cout << "2" << std::endl;
}
  • 尽管指针所指的对象类型是一个含有虚函数的类,但是指针本身并不是一个类类型的对象类型Base*将在编译时求值,显然它与Derived不同

  • typeid是否需要运行时检查决定了表达式是否会被求值:
    • 只有当类型含有虚函数时,编译器才会对表达式求值
    • 反之,如果类型不含有虚函数,则typeid返回表达式的静态类型;编译器无法对表达式求值也能知道表达式的静态类型
  • 如果表达式的动态类型可能与静态类型不同,则必须在运行时对表达式求值以确定返回的类型。这条规则适用于typeid(*p)的情况:
    • 如果指针p所指的类型不含有虚函数,则p不必非得是一个有效的指针
    • 否则,*p将在运行时求值,此时p必须是一个有效的指针
    • 如果p是一个空指针,则typeid(*p)将抛出一个名为bad_typeid的异常

三、使用RTTI

RTTI在实际中的一种应用

  • 在某些情况下RTTI非常有用:
    • 比如当我们想为具有继承关系的类实现相等运算符时
    • 对于这两个对象来说,如果它们的类型相同并且对应的数据成员取值相同,则我们说这两个对象是相等的
    • 在类的继承体系中,每个派生类负责添加自己的数据成员,因此派生类的相等运算符必须把派生类的新成员考虑进来
  • 一种容易想到的解决方案是定义一套虚函数:
    • 令其在继承体系的各个层次上分别执行相等性判断。此时,我们可以为基类的引用定义一个相等运算符,该运算符将它的工作委托给虚函数(假设名为equal),由equal负责实际的操作。
    • 但是上述方案很难奏效。虚函数的基类版本和派生类版本必须具有相同的形参类型。如果我们想定义一个虚函数equal,则该函数的形参必须是基类的引用。此时,equal函数将只能使用基类的成员,而不能比较派生类独有的成员
  • 想要真正有效的相等比较操作,我们可以使用RTTI机制:
    • 我们定义的相等运算符的形参时基类的引用,然后使用typeid检查两个运算对象的类型是否一致
    • 如果运算对象的类型不一致,则==返回false。类型一致才调用equal函数,每个类定义的equal函数负责比较类型自己的成员
    • 例如:==运算符接受Base&形参,但是在进行比较操作前先把运算对象转换成运算符所属的类类型

演示案例

  • 为了更好地解释上面的概念,我们定义下面两个类:
class Base {
public:
    friend bool operator==(const Base&, const Base&);
public:
    //类的接口成员
protected:
    virtual bool equal(const Base&)const;
};

class Derived :public Base {
public:
    //类的接口成员
protected:
    bool equal(const Base&)const;
};
  • 接下来我们介绍如何定义整体的相等运算符:
bool operator==(const Base& lhs, const Base& rhs)
{
    //如果typeid不相同返回false,&&后面的虚函数就不调用了
    //如果typeid返回true,继续执行equal虚函数
    return (typeid(lhs) == typeid(rhs)) && (lhs.equal(rhs));
}
  • 上述代码中:
    • 如果运算对象的类型不同则返回false,后面的虚函数就不调用了
    • 如果运算对象的类型相同返回true,继续调用后面的虚函数equal
      • 当运算对象是Base的对象时,调用Base::equal
      • 当运算对象时Derived的对象时,调用Derived::equal
  • 派生类的equal虚函数:
    • 继承体系中的每个类必须定义自己的equal函数。派生类的所有函数要做的第一件事都是相同的,就是将实参的类型转换为派生类类型
    • 例如Derived派生类中的equal虚函数定义如下:
bool Derived::equal(const Base& rhs)const
{
    auto r = dynamic_cast<const Derived&>(rhs);
    //执行比较两个Derived对象的操作并返回结果
}
  • 基类的equal虚函数:
    • 基类中的虚函数定义比较简单
    • *this和形参都是Base对象,因此当前对象直接比较即可,不需要类型转换
bool Base::equal(const Base& rhs)const
{
    //直接比较Base对象的操作
}

四、type_info类

  • type_info类的精确定义随着编译器的不同而略有差异。不过,C++标准规定type_info必须定义在typeinfo头文件中
  • type_info定义了下面的操作:

  • 因为type_info类一般作为一个基类出现,所以它还应该提供一个公有的虚析构函数。当编译器希望提供额外的类型信息时,通常在type_info的派生类中完成
  • type_info类的一些特点:
    • type_info没有默认构造函数
    • type_info的拷贝和移动构造函数、以及赋值运算符都被定义成删除的。因此,我们无法定义或拷贝type_info类型的对象,也不能为type_info类型的对象赋值
    • 创建type_info对象的唯一途径是使用typeid运算符

name()成员函数

  • type_info类的name成员函数返回一个C风格字符串,表示对象的类型名字
  • 对于某种给定的类型来说,name的返回值因编译器而异并且不一定与在程序中使用的名字一致
  • 演示案例:
#include <iostream>
#include <string>
#include <vector>
#include <new>
using namespace std;

class Base {
public:
    virtual void foo() {}
};

class Derived :public Base {};

int main()
{
    int arr[10];
    Derived d;
    Base *p = &d;

    std::cout << "42:      " << typeid(42).name() << std::endl;
    std::cout << "arr:     " << typeid(arr).name() << std::endl;
    std::cout << "string:  " << typeid(std::string).name() << std::endl;
    std::cout << "d:       " << typeid(d).name() << std::endl;
    std::cout << "p:       " << typeid(p).name() << std::endl;
    std::cout << "*p:      " << typeid(*p).name() << std::endl;
    std::cout << "Base:    " << typeid(Base).name() << std::endl;
    std::cout << "Derived: " << typeid(Derived).name() << std::endl;
    return 0;
}

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

董哥的黑板报

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值