RTTI(Run-Time Type Information)运行时类型识别的英文缩写,它提供了运行时确定对象类型的方法。面向对象的编程语言,像C++,Java,delphi都提供了对RTTI的支持。 本文将描述 RTTI 的概念、简略介绍 RTTI 的一些背景知识、并通过具体例子和代码介绍什么时候使用以及如何使用 RTTI的两个重要运算符 typeid 和 dynamic_cast的使用方法。
其实,RTTI 在C++中并不是什么新的东西,它早在十多年以前就已经出现了。但是大多数开发人员,包括许多高层次的C++程序员对它并不怎么熟悉,更不用说使用 RTTI 来设计和编写应用程序了。
一些面向对象专家在传播自己的设计理念时,大多都主张在设计和开发中明智地使用虚拟成员函数,而不用 RTTI 机制。但是,在很多情况下,虚拟函数无法克服本身的局限。每每涉及到处理异类容器和根基类层次(如 MFC)时,不可避免要对对象类型进行动态判断,也就是动态类型的侦测。如何确定对象的动态类型呢?答案是使用内建的 RTTI 中的运算符:typeid 和 dynamic_cast。
1.type_info
type_info类在头文件
中定义,代表了一个C++类型的相关信息。一般由typeid操作符返回,不能自己构造。
type_info是实现相关的,不同编译工具链的实现可能不一致。
下面的代码可以打印出int类型的名字:
const std::type_info &tiInt = typeid(int);
std::cout << "tiInt.name = " << tiInt.name() << std::endl;
type_info有下列方法:
2.typeid操作符
typeid操作符在
中声明,用来在运行时获取类型、变量、表达式的类型信息,适用于C++基础类型、内置类、用户自定义类、模板类等。typeid的操作是在编译时期就已经决定的了。
它有两种形式:
具体用法前面的示例代码已有了。
3.type_index(C++11 新特性)
type_index类在头文件
下面的代码(来自http://en.cppreference.com/w/cpp/types/type_index)使用type_index来输出一些类型信息(调试还是蛮有用的):
struct A {
virtual ~A() {}
};
struct B : A {};
struct C : A {};
int main()
{
std::unordered_map type_names;
type_names[std::type_index(typeid(int))] = "int";
type_names[std::type_index(typeid(double))] = "double";
type_names[std::type_index(typeid(A))] = "A";
type_names[std::type_index(typeid(B))] = "B";
type_names[std::type_index(typeid(C))] = "C";
int i;
double d;
A a;
// note that we're storing pointer to type A
std::unique_ptr b(new B);
std::unique_ptr c(new C);
std::cout << "i is " << type_names[std::type_index(typeid(i))] << '\n';
std::cout << "d is " << type_names[std::type_index(typeid(d))] << '\n';
std::cout << "a is " << type_names[std::type_index(typeid(a))] << '\n';
std::cout << "b is " << type_names[std::type_index(typeid(*b))] << '\n';
std::cout << "c is " << type_names[std::type_index(typeid(*c))] << '\n';
}
4.dynamic_cast
它究竟是怎么实现的呢?经过一些思考,我认为最简单的方案就是将信息保存在vtable里,它会占用一个vtalbe表的项目。实验和书籍也证明了这一点。但是就会有一个问题,没有vtable的类怎么办?内建类型怎么办?其实,没有vtable的类,它不需要多态,它根本就不需要RTTI,内建类型也一样。这就是说,dynamic_cast只支持有虚函数的类。而且, dynamic_cast不能进行non_base_class *到 class T*的转换,比如void * --> class T *,因为它无法去正确获得vtable。
这样,dynamic_cast的意义和使用方法就很清楚了,它是为了支持多态而存在的。它用于实现从基类到派生类的安全转换。同时它也在绝大多数情况下避免了使用static_cast——不安全的类型转换。
对于没有virtual函数的类而言,typeid是编译时期的事情(也就是静态类型);对于有virtual函数的类而言,typeid是运行时期的事情(也就是动态类型)。
为什么是这样呢?因为RTTI(typeid和dynamic_cast都是RTTI的一部分)大部分都是要利用virtual table来实现的,比如在virtual table的第0个索引处存放指向type_info信息的指针。所以如果没有virtual function,那么此class就没有virtual table,所以RTTI也自然就不能正常运行了(那么typeid也就自然不能够正常运行了,而只能根据编译时期的静态类型来进行判断)。
在c++里怎么能知道一个变量的具体类型。
1.运行时获知变量类型名称,可以使用 typeid(变量).name,需要注意不是所有编译器都输出"int"、"float"等之类的名称,对于这类的编译器可以这样使用:float f = 1.1f; if( typeid(f) == typeid(0.0f) )
2.对于多态类实例,想得到实际的类名称,需要使用到RTTI,这需要在编译的时候加上参数"/GR"。
3.对于普通变量,既然是自己写的,那当然也就应该知道它的类型,其实用不着运行时获知;对于多态类实例,既然需要运行时获知实际类型,那么就说明这里不具有多态性,既然没有多态性就不应该抽象它,这属于设计错误,总之,我认为RTTI是多余的。
4.对于多态类实例,使用 typeid(value) == typeid(value)来判断,不如使用 dynamic_cast 来判断,它们的原理是一样的。
首先让我们来设计一个类层次,假设我们创建了某个处理文件的抽象基类。它声明下列纯虚函数:open()、close()、read()和 write():
class File{
public:
virtual int open(const string & filename)=0;
virtual int close(const string & filename)=0;
virtual ~File()=0; // 记住添加纯虚拟析构函数
};
现在从 File 类派生的类要实现基类的纯虚拟函数,同时还要提供一些其他的操作。假设派生类为 DiskFile,除了实现基类的纯虚拟函数外,还要实现自己的flush()和defragment()操作:
class DiskFile: public File{
public:
int open(const string & filename);
// 实现其他的纯虚拟函数
......
// 自己的专有操作
virtual int flush();
virtual int defragment();
};
接着,又从 DiskFile 类派生两个类,假设为 TextFile 和 MediaFile。前者针对文本文件,后者针对音频和视频文件:
class TextFile: public DiskFile{
// ......
int sort_by_words();
};
class MediaFile: public DiskFile{
//......
};
我们之所以要创建这样的类层次,是因为这样做以后可以创建多态对象,如:
File *pfile; // *pfile的静态类型是 File
if(some_condition)
pfile = new TextFile; // 动态类型是 TextFile
else
pfile = new DiskFile; // 动态类型是 DiskFile
假设你正在开发一个基于图形用户界面(GUI)的文件管理器,每个文件都可以以图标方式显示。当鼠标移到图标上并单击右键时,文件管理器打开一个菜单,每个文件除了共同的菜单项,不同的文件类型还有不同的菜单项。如:共同的菜单项有“打开”“拷贝”、和“粘贴”,此外,还有一些针对特殊文件的专门操作。比如,文本文件会有“编辑”操作,而多媒体文件则会有“播放”菜单。为了使用 RTTI 来动态定制菜单,文件管理器必须侦测每个文件的动态类型。利用 运算符 typeid 可以获取与某个对象关联的运行时类型信息。typeid 有一个参数,传递对象或类型名。因此,为了确定 x 的动态类型是不是Y,可以用表达式:typeid(x) == typeid(Y)实现:
#include // typeid 需要的头文件
void menu::build(const File * pfile){
if (typeid(*pfile)==typeid(TextFile))
add_option("edit");
else if (typeid(*pfile)==typeid(MediaFile))
add_option("play");
}
使用 typeid 要注意一个问题,那就是某些编译器(如 Visual C++)默认状态是禁用 RTTI 的,目的是消除性能上的开销。如果你的程序确实使用了 RTTI,一定要记住在编译前启用 RTTI。使用 typeid 可能产生一些将来的维护问题。假设你决定扩展上述的类层次,从MediaFile 派生另一个叫 LocalizeMedia 的类,用这个类表示带有不同语言说明文字的媒体文件。但 LocalizeMedia 本质上还是个 MediaFile 类型的文件。因此,当用户在该类文件图标上单击右键时,文件管理器必须提供一个“播放”菜单。可惜 build()成员函数会调用失败,原因是你没有检查这种特定的文件类型。为了解决这个问题,你必须象下面这样对 build() 打补丁:
void menu::build(const File * pfile){
//......
else if (typeid(*pfile)==typeid(LocalizedMedia))
{
add_option("play");
}
}
唉,这种做法真是显得太业余了,以后每次添加新的类,毫无疑问都必须打类似的补丁。显然,这不是一个理想的解决方案。这个时候我们就要用到 dynamic_cast,这个运算符用于多态编程中保证在运行时发生正确的转换(即编译器无法验证是否发生正确的转换)。用它来确定某个对象是 MediaFile 对象还是它的派生类对象。dynamic_cast 常用于从多态编程基类指针向派生类指针的向下类型转换。它有两个参数:一个是类型名;另一个是多态对象的指针或引用。其功能是在运行时将对象强制转换为目标类型并返回布尔型结果。也就是说,如果该函数成功地并且是动态的将 *pfile 强制转换为 MediaFile,那么 pfile的动态类型是 MediaFile 或者是它的派生类。否则,pfile 则为其它的类型:
void menu::build(const File * pfile)
{
if (dynamic_cast (pfile))
{
// pfile 是 MediaFile 或者是MediaFile的派生类 LocalizedMedia
add_option("play");
}
else if (dynamic_cast (pfile))
{
// pfile 是 TextFile 是TextFile的派生类
add_option("edit");
}
}
细细想一下,虽然使用 dynamic_cast 确实很好地解决了我们的问题,但也需要我们付出代价,那就是与 typeid 相比,dynamic_cast 不是一个常量时间的操作。为了确定是否能完成强制类型转换,dynamic_cast`必须在运行时进行一些转换细节操作。因此在使用 dynamic_cast 操作时,应该权衡对性能的影响。
如果你不知道一个对象的确定类型,RTTI可以告诉你。但是有个限制:这个类型在编译期间必须已知,才能使用RTTI来识别它。例如,在磁盘上或者网络中的一段字符串,被告知代表一个类,但是编译器在编译代码的时候,并不知道,怎么才能使用这个类呢?这时候就需要使用反射。
通过反射与未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类(与RTTI一样)。但在这之后,在做其它事情之前,必须加载那个类的 Class 对象。因此,那个类的.class 文件对于 JVM 来说必须是可获取的,要么在本地机器上,要么可以通过网络取得。
RTTI:编译器在编译期间打开和检查.class文件
反射:编译器在运行期间打开和检查.class文件
整理自:C++ RTTI及“反射”技术与C++11 | 运行时类型识别(RTTI)