C++ RTTI 详解:动态类型识别的奥秘
文章目录
- 前置知识
- RTTI简介
- RTTI的使用
- 1.typeid
- 2.dynamic_cast
- 3.RTTI与代码调试
- RTTI实现原理
- RTTI的使用场景
- RTTI的局限性
- RTTI拓展
- RTTI典型的应用需求
- RTTI实现的要点
- RTTI(反射)的其它实现
前置知识
- 多态
- 虚表
- 类型转化
RTTI简介
RTTI(Runtime Type Identification)是“运行时类型识别”的意思,主要作用是在多态对象中实现类型安全的动态转换。C++引入这个机制是为了让程序在运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型。但是现在RTTI的类型识别已经不限于此了,它还能通过typeid操作符识别出所有的基本类型的变量对应的类型。为什么会出现RTTI这一机制呢?这和C++语言本身有关系,C++是一门静态类型语言,其数据类型是在编译期就确定的,不能在运行时更改。然而由于面向对象程序设计中多态性的要求,C++中的指针或引用本身的类型,可能与它实际代表的类型并不一致,有时我们需要将一个多态指针转换为其实际指向对象的类型,就需要知道运行时的类型信息,这就有了运行时类型识别需求。和Java[反射]相比,C++要想获得运行时类型信息,只能通过RTTI机制,并且C++最终生成的代码是直接与机器相关的。
注释:
- Python: PyTypeObject
- Java: 反射=> 字节码中的元数据
- JS: 原型链(类似于C++虚表)
- C#: CLR(公共语言运行时)
RTTI的使用
C++通过以下两个关键字提供RTTI功能:
- typeid:该运算符返回其表达式或类型名的实际类型
- dynamic_cast:该运算符将基类的指针或引用安全地转换为派生类类型的指针或引用(也就是所谓的下行转换)
1.typeid
语法:typeid (type) 、typeid (expression)即任意表达式或类型名。
常见的用途:比较两个表达式的类型,或者将表达式的类型与特定类型相比较。
返回类型:const type_info&;
class type_info{
public:
virtul ~type_info();
bool operator == (const type_info&rhs)const;
bool operator != (const type_info&rhs)const;
bool before(const type_info&rhs)const;
const char* name()const;
private:
type_info(const type_info& rhs);
type_info& operator=(const type_info& rhs);
}
/*
接口说明:
operator ==和operator!=:比较操作符,返回两个类型是否为(或不为)同一类型(注:基类和派生类不为同一类型!)。
before:若在类型排序中,该类型先于rhs的类型则返回true。
name:返回类型对应的名字(具体所用的值,依赖于具体编译器)(以\0结束的字符串)。
注意:type_info类的默认构造函数和复制构造函数以及赋值操作符都定义为private,故不能定义或复制type_info类型的对象。程序中创建type_info对象的唯一方法是使用typeid操作符。
*/
代码例子:
#include <iostream>
#include <typeinfo>
using namespace std;
struct Base {};
struct Derived : Base {};
struct Poly_Base {virtual void Member(){}};
struct Poly_Derived: Poly_Base {};
int main() {
int a;
int * pa;
cout << "int is: " << typeid(int).name() << endl;
cout << " a is: " << typeid(a).name() << endl;
cout << " pa is: " << typeid(pa).name() << endl;
cout << "*pa is: " << typeid(*pa).name() << endl << endl;
Derived derived;
Base* pbase = &derived;
cout << "derived is: " << typeid(derived).name() << endl;
cout << "*pbase is: " << typeid(*pbase).name() << endl;
cout << "same type? ";
cout << ( typeid(derived)==typeid(*pbase) ) << endl << endl;
Poly_Derived polyderived;
Poly_Base* ppolybase = &polyderived;
cout << "polyderived is: " << typeid(polyderived).name() << endl;
cout << "*ppolybase is: " << typeid(*ppolybase).name() << endl;
cout << "same type? ";
cout << ( typeid(polyderived)==typeid(*ppolybase) ) << endl << endl;
return 0;
}
// 输出
/*
int is: int
a is: int
pa is: int *
*pa is: int
derived is:struct Derived
pbase is: struct Base
same type? 0
polyderived is:struct Poly Derived
*ppolybase is: struct Poly Derived
same type? 1
*/
注释:
标准C++规定,type_info类的确切定义随编译器而变化,只要保证所有的实现提供以上的基本操作就行(见类定义)。即具体实现细节,各编译器厂商可自行决定。
2.dynamic_cast
语法形式:dynamic_cast(v) ,将对象 v 转换为类型T的对象。
前提:v 要么为指向其派生类对象的基类指针,要么为引用其派生类对象的基类对象。否则,v 返回nullptr(为指针时)或抛出std::bad_cast异常(为引用类型时);而T为所期望的派生类指针类型或派生类引用类型。且v指向的基类里必须包含虚函数,即多态类型,否则编译出错。
常用写法:
(1)Poly_Derived* derivedPtr = dynamic_cast<Poly_Derived*>(ppolybase);//转换为指向Poly_Derived 型的指针,失败返回nullptr;
(2)Poly_Derived& derivedRef = dynamic_cast<Poly_Derived&>(polyderived); //转换为Poly_Derived 引用,失败时抛出bad_cast异常。
代码例子:
#include <iostream>
#include <typeinfo>
using namespace std;
struct Poly_Base {virtual void Member(){}};
struct Poly_Derived: Poly_Base {};
int main() {
Poly_Derived polyderived;
Poly_Base* ppolybase = &polyderived;
Poly_Base& rpolybase = polyderived;
if(Poly_Derived* derivedPtr = dynamic_cast<Poly_Derived*>(ppolybase))//base pointer {
cout<<"dynamic_cast pointer success."<<endl;
}
else
{
cout<<"dynamic_cast pointer fail!"<<endl;
}
try
{
const Poly_Derived& derivedRef = dynamic_cast<const Poly_Derived&>(rpolybase);
cout<<"dynamic_cast reference success."<<endl;
}
catch(bad_cast)
{
cout<<"dynamic_cast reference fail."<<endl;
}
cout <<"same type? ";
cout << ( typeid(rpolybase)==typeid(*ppolybase) ) << endl;
return 0;
}
// 输出
/*
dynamic_cast pointer success.
dynamic_cast reference success.
same type? 1
*/
3.RTTI与代码调试
如果变量涉及多态,调试器可以利用 RTTI(运行时类型识别) 获取对象的真实类型。
- 确认对象的真实类型
- 发现对象切片(Object Slicing)问题
RTTI实现原理
-
内部数据结构:
RTTI(Run-Time Type Information)依赖于编译器生成的元数据来提供运行时的类型信息。在 C++ 中,RTTI 主要依靠虚函数表(vtable)实现,对应的核心数据结构包括:- 类型信息指针(type_info):每个多态类(至少包含一个虚函数的类)都会在 vtable 中存储一个指向 std::type_info 结构的指针,该结构保存了该类的类型信息。
- 虚函数表(vtable):对象的 vtable 存储了虚函数的指针,以及类型信息指针(通常位于 vtable 的起始位置)。
[图片]
-
编译器生成的元数据:
当编译器启用 RTTI(通常默认启用)时,它会为每个包含虚函数的类生成额外的类型信息。这些信息被嵌入到二进制文件中,并在运行时用于类型识别。 -
运行时动态查找(dynamic_cast):
- 允许将基类指针/引用转换为派生类指针/引用。
- 依赖 vtable 查找实际的类型信息,若转换失败,指针返回 nullptr,引用则抛出 std::bad_cast 异常。
RTTI的使用场景
- 常见应用
- 安全类型转换:在多态场景下,dynamic_cast 可以确保安全的向下转换,避免 reinterpret_cast 可能导致的未定义行为。
- 调试与日志:在日志系统中使用 typeid 获取对象类型,帮助调试动态类型的行为。
- 实践建议
- 何时使用 RTTI:适用于需要在运行时判断对象类型的场景,例如插件系统、序列化框架。
- 何时避免 RTTI:
- 性能敏感场景(例如嵌入式系统或高性能应用),因为 dynamic_cast 需要额外的 vtable 查找开销。
- 设计可以通过虚函数、多态模式避免直接使用 RTTI 的场景。
RTTI的局限性
- 性能开销:
- RTTI 需要额外的存储空间(type_info 结构、vtable 指针)。
- dynamic_cast 可能涉及多次 vtable 查找,影响执行效率
- 局限性:
- 仅适用于多态类型(具有虚函数的类),不支持非多态类型的运行时识别。
- 过度依赖 RTTI 可能意味着设计问题,通常可以通过更好的设计模式来避免直接使用 RTTI。
- 替代方法:
- 手动类型标记:
- 通过在基类中定义 enum 或 ID 标识类型。
- 模板编程:
- 例如 std::variant 和 std::any,可实现静态类型安全。
- CRTP(Curiously Recurring Template Pattern):
- 通过模板参数静态分派类型信息,减少 RTTI 依赖,提高性能。
- 手动类型标记:
RTTI拓展
RTTI典型的应用需求
1、类型的识别,即能在运行时判断出某对象、表达式等的类型,能判断它们是基本类型(int、string),还是对象,以及它们区别于其它类型的标识;
2、对象的继承关系的运行时判断;
3、在出错处理、内存诊断等处理时的输出信息;
4、基于字符型名称的运行时对象访问、方法调用;
5、对象的自动保存和读入;
6、基于ID或名称的对象自动生成;
7、环境配置的保存和读入;
8、程序自动生成;
RTTI实现的要点
1、必须满足特定语言的定位和要求。不能说,将所有可能的功能(类型 成员变量 成员函数 继承关系)加进去就是好东西了;
2、尽可能透明的:RTTI的主要应用在IDE和底层。一般情况下,编程用户不需要了解过多的RTTI细节。
3、尽可能轻便:不能因为实现RTTI要耗费大量内存和CPU时间,不能占用太大的程序空间,
RTTI(反射)的其它实现
- MFC: MFC实现了简单的,与标准C++有区别的RTTI,其实现是通过宏来完成的,同时通过宏来辅助用户完成执久化工作,但执久化的具体细节要求用户完成。
- VCL: VCL实现了比较复杂的RTTI,具有关系继承、属性、方法、事件、基于属性的自动对象生成、强大的执久化功能;
- COM: COM通过类型库定义其RTTI,比VCL还要复杂
- QT: Q_OBJECT机制,在编译前,必须将其RTTI信息转换为标准C++语言,静态成员变量/函数
- WXWINDOW: 实现的是比较简单的RTTI。
- VCF: 实现了我所看到的在C++中最全面的RTTI功能,但其性能是一个大问题。
- OOPS: http://www.rcs.hu/Articles/RTTI_Part1.html