C++ 是一种静态类型语言,数据类型在编译时确定。但在有些场景下,编译时无法确定数据类型,需要在运行时才能确定。RTTI(Run Time Type Identification,运行时类型识 别)就是一种能够在运行时动态确定数据类型的机制。
- RTTI 应用场景 {#title-0}
C++ 中使用 typeid 和 dynamic_cast 时,会涉及到运行时类型识别的支持。接下来,我们分析下这两种应用场景。
1.1 typeid {#title-1}
通过 typeid 运算符可以获得变量的类型。此时,需要重点注意的是,typeid 可以在编译期将获得变量的类型,也可以在运行期获得变量的类型。
请看下面的示例代码(编译期获得变量的类型):
#include <iostream>
using namespace std;
class Box {};
void test()
{
// 获得基本类型变量的类型信息
int a = 10;
cout << typeid(a).name() << endl; // 输出:int
// 获得自定义对象的类型信息
Box box;
cout << typeid(box).name() << endl; // 输出:Box
int* p = &a;
cout << typeid(*p).name() << endl; // 输出:int
Box* pBox = new Box;
cout << typeid(*pBox).name() << endl; // 输出:Box
}
int main()
{
test();
return 0;
}
上述代码中,编译器通过分析代码上下文,对象 a 和 box 的类型在编译期就可以确定。
什么情况下,typeid 需要在运行期获得变量的类型呢?
#include <iostream>
using namespace std;
class Animal {
public:
virtual ~Animal() = default;
};
class Bee : public Animal {};
class Dog : public Animal {};
void test()
{
Animal* animal = new Bee;
cout << typeid(*animal).name() << endl; // Bee
animal = new Dog;
cout << typeid(*animal).name() << endl; // Dog
}
int main()
{
test();
return 0;
}
程序执行结果:
class Bee
class Dog
**重点注意:**Animal 类的内部必须包含虚函数(我们写的是虚析构函数),否则的话,typeid 运算符会在编译期根据 animal 指针的类型确定 *animal 为 Animal 类型,并不会在运行期确定类型。
1.2 dynamic_cast {#title-2}
dynamic_cast 能够去检验具有继承关系的父子类型的指针、引用的转换是否安全。
dynamic_cast 也分为编译期类型转换、运行期类型转换。请看下面的示例代码(编译期类型转换):
#include <iostream>
using namespace std;
class Animal {};
class Dog : public Animal {};
void test()
{
Dog* dog = new Dog;
// 子类指针转换为父类指针,安全类型转换
Animal* animal = dynamic_cast<Animal*>(dog);
cout << animal << endl;
}
int main()
{
test();
return 0;
}
上述代码中,将一个较大寻址范围的指针转换为较小的范围,不会导致内存越界操作,所以是安全的、允许的、也可以在编译期完成。但是,请看下面的示例代码(动态类型转换):
#include <iostream>
using namespace std;
class Animal {
public:
virtual ~Animal() = default;
};
class Dog : public Animal {};
void test()
{
// 将 Animal 类型指针转换为 Dog 类指针
Animal* animal = nullptr;
Dog* dog = nullptr;
animal = new Animal;
dog = dynamic_cast<Dog*>(animal);
cout << dog << endl; // 输出:0, 转换失败
animal = new Dog;
dog = dynamic_cast<Dog*>(animal);
cout << dog << endl; // 输出:非0, 转换成功
}
int main()
{
test();
return 0;
}
程序执行结果:
0000000000000000
0000028889BF3930
尝试将 Animal(小) 类型的指针转换成 Cat(大) 类型的。此时:
- 如果 animal 指针指向的是 Cat 类型的对象,是安全的。
- 如果 animal 指针指向的是 Animal 类型的对象,是不安全的。
重点注意:如果希望 dynamic_cast 能够进行动态的类型检查,Animal 类中必须包含虚函数,即:多态。否则,编译器不允许 dynamic_cast 将一个父类类型的指针转换成子类类型(向下类型转换)。
- RTTI 和虚函数 {#title-3}
typeid、dynamic_cast 在进行运行期类型识别时,依赖于虚函数机制。所以,C++ RTTI 是以虚函数机制作为支撑,实现的动态类型识别。
为什么 RTTI 会和虚函数有关联?
在 C++ 中,大部分情况下,定义的对象类型是明确的,编译期可确定的。但是,在发生多态的时候,就可能会出现基类 B 类型指针指向派生类 D 类型对象的情况。
此时,想要获得对象类型,就需要在对象中安插额外的信息。既然这种场景发生在多态场景下,那干脆就把信息合并到虚函数表中,减少复杂度。
RTTI 如何基于虚函数机制来实现动态类型识别?
当一个类包含至少一个虚函数时,编译器会为这个类生成一个虚函数表。虚函数表中的第一个指针通常指向 std::type_info
对象。这使得可以通过 vfptr 访问到对象的类型信息。请看下面的代码:
#include <iostream>
#include <Windows.h>
using namespace std;
class Animal {
public:
virtual void show() {};
};
class Dog: public Animal {};
void test()
{
Animal* animal = new Dog;
cout << typeid(*animal).name() << endl;
}
int main()
{
test();
return 0;
}
上述代码,在 VS2022 中得到的对象结构:
// Animal 类
class Animal size(4):
+---
0 | {vfptr}
+---
Animal::$vftable@:
| &Animal_meta
| 0
0 | &Animal::show
// Cat 类
class Dog size(4):
+---
0 | +--- (base class Animal)
0 | | {vfptr}
| +---
+---
Dog::$vftable@:
| &Dog_meta
| 0
0 | &Animal::show
在虚函数表内部,都会保存一个 &Xxx_meta
指针,该指针指向了对象的类型信息。通过动态查询对象的类型信息来确保指针转换的安全性。