11、RTTI与typeid:运行时类型识别
说到多态,很多人第一反应就是虚函数表。但有个问题我一直觉得挺有意思——你拿到一个基类指针,怎么知道它到底指向什么类型?
嗯,这就是RTTI(Runtime Type Identification)要干的事。说白了,就是让程序在运行的时候,能认出对象的真实身份。
RTTI的原理:藏在虚表里的“身份证”
RTTI不是凭空变出来的。它依赖虚函数表。每个有虚函数的类,编译器都会在虚表里塞一个额外的指针——指向一个type_info对象。
我刚开始学的时候,以为RTTI是个很玄乎的东西。后来看了反汇编才明白,其实就是查表。你调用typeid,编译器就帮你从虚表里把这个指针取出来。
核心要点:只有多态类型(有虚函数的类)才能用RTTI。普通类用typeid,结果是在编译期就确定的。
举个例子:
class Base {
public:
virtual ~Base() {}
};
class Derived : public Base {};
Base* p = new Derived;
// 这里typeid会去查虚表,拿到Derived的type_info
const std::type_info& info = typeid(*p);
你看,typeid(*p)返回的是Derived的类型信息。如果去掉virtual,那返回的就是Base了——因为编译器在编译期就决定了。
typeid运算符:怎么用?
typeid用起来很简单。它返回一个std::type_info的引用。你可以用它做两件事:
- 比较两个类型是否相同
- 获取类型的名字(不过这个名字是编译器决定的,不一定好看)
#include <typeinfo>
#include <iostream>
Base* b1 = new Base;
Base* b2 = new Derived;
if (typeid(*b1) == typeid(*b2)) {
std::cout << "same type" << std::endl;
} else {
std::cout << "different types" << std::endl;
// 输出:different types
}
// 看看名字
std::cout << typeid(*b2).name() << std::endl;
// 输出可能是 "7Derived"(取决于编译器)
个人经验:我在做序列化框架时,经常用typeid来区分对象类型。但要注意,.name()返回的字符串格式是编译器相关的,别拿它做跨平台比较。
dynamic_cast:安全的向下转型
dynamic_cast是RTTI最常用的场景。你想把基类指针转成派生类指针,但又怕转错了——这时候就用dynamic_cast。
它的原理其实不复杂:
- 先通过虚表拿到对象的真实类型
- 检查目标类型是不是真实类型的基类
- 如果是,就返回正确的指针;否则返回
nullptr
Base* p = new Derived;
// 安全的向下转型
Derived* d = dynamic_cast<Derived*>(p);
if (d) {
// 转型成功,可以安全使用d
d->derivedMethod();
} else {
// 转型失败
std::cout << "不是Derived类型" << std::endl;
}
// 如果转成不相关的类型
class Other {};
Other* o = dynamic_cast<Other*>(p); // 返回nullptr
我曾经踩过的坑:有一次我忘了检查dynamic_cast的返回值,直接用了转型后的指针。结果程序在特定路径下崩溃了——因为那个对象根本不是我想的类型。从那以后,我养成了习惯:每次dynamic_cast之后,一定检查返回值。
type_info结构:里面有什么?
std::type_info是个很小的类。它主要提供这几个接口:
| 接口 | 说明 |
|---|---|
name() |
返回类型名称(编译器相关) |
operator== |
比较两个类型是否相同 |
operator!= |
比较两个类型是否不同 |
before() |
用于排序(实现<语义) |
hash_code() |
C++11引入,返回哈希值 |
你想想看,这个结构其实很轻量。它不存储对象的任何数据,只存一个标识类型的内部指针。所以用typeid的开销很小——就是一次虚表查找。
// type_info的典型用法
const std::type_info& t1 = typeid(int);
const std::type_info& t2 = typeid(double);
if (t1 == t2) {
// 不会执行
}
// 用于map的key
std::map<const std::type_info*, std::string> typeNames;
typeNames[&typeid(int)] = "int";
typeNames[&typeid(double)] = "double";
我个人的习惯:在调试阶段,我会用typeid(*obj).name()来打印对象类型。这比猜指针指向什么类型靠谱多了。但正式代码里,我尽量少用RTTI——它破坏了封装,而且有些项目会禁用RTTI来减小二进制体积。
RTTI的性能开销
很多人担心RTTI慢。其实没那么夸张。每次typeid或dynamic_cast的开销,大概就是:
- 一次虚表指针解引用
- 一次类型信息查找
- (对于
dynamic_cast)一次类型继承链的遍历
说白了,比虚函数调用稍微重一点,但远没到需要担心的程度。我做过测试,在百万次调用级别,RTTI的开销也就几毫秒。
不过要注意:dynamic_cast在多重继承下会慢一些,因为要遍历多条继承链。但大多数场景下,你根本感觉不到。
什么时候该用RTTI?
说实话,RTTI是个双刃剑。用得好,能解决很多棘手问题;用不好,代码会变得难以维护。
我个人觉得,这几个场景适合用:
- 序列化/反序列化框架
- 插件系统(需要动态识别对象类型)
- 调试工具和日志系统
- 某些设计模式的实现(比如Visitor模式)
但如果你发现自己频繁用dynamic_cast和typeid,那可能说明设计有问题。更好的做法是用虚函数来替代——让对象自己处理,而不是你去猜它的类型。
记住:RTTI是工具,不是银弹。能用多态解决的问题,就别用RTTI。
本章小结
RTTI的原理其实不复杂——就是利用虚表里藏的type_info指针。你学会了吗?
typeid返回type_info引用,用于比较和获取类型名dynamic_cast做安全的向下转型,失败返回nullptrtype_info结构很轻量,主要提供name()和比较操作- 性能开销可接受,但别滥用
嗯,RTTI就这些。下一章我们聊聊虚函数表的内部布局——那个更有意思。