C++ RTTI与反射

Share on:
200 Words | Read in about 1 Min | View times

Overview

前文《对象内存模型》在介绍C++对象内存模型中有提到过type_info对象。type_info是RTTI机制的核心内容。那什么是RTTI?反射和RTTI又有什么关系?本节内容将一一解答。

本系列文章将包括以下领域:

本章其他内容请见 《现代C++》

什么是RTTI?

C++是如何通过基类指针或引用,得知所指向的对象的实际类型?

这是一道高频面试题,这个题目考察的是RTTI原理。

RTTI,Runtime Type Identification,运行时类型识别,是程序在运行时能够检查基类指针或引用所指向的派生类的实际类型。

C++是一门静态类型语言,所有的数据类型都是在编译期确定,不能在运行时修改。而C++为了能够支持多态的特性,在语言层面,是可以通过一个基类类型的指针或引用,指向一个实际上与基类类型并不一致的类型,但是这个类型必须是基类的派生类。

当我们需要将一个多态的指针或引用转换成它实际指向的对象类型,就需要知道运行时的类型信息,这就产生了RTTI的需求了,这时候这种多态称为动态多态。与动态多态相对应的是静态多态,C++通过模板和重载实现静态多态,在编译时就能确定对象的类型信息。

其实,RTTI也有一些缺点:

  • 破坏抽象,使一些本来不该被使用的函数和数据被不正确地使用

  • 运行时的不确定性,容易导致程序健壮性差

  • 使得程序扩展性变差,当加入一个新类型时,需要特别小心dynamic_cast相关的代码,必要时还需要修改,以确保新加入类型不会出现转换异常

C++的RTTI通过两个操作符来实现,即typeid操作符和dynamic_cast操作符。dynamic_cast是C++的四种强制类型转换方法之一,其他类型转换方法可以阅读前面的文章《类型转换》

typeid操作符

typeid操作符返回的结果是一个的type_info类型对象的常量引用,代表表达式或类型名的实际类型。typeid在标准库头文件<typeinfo>中定义,有如下两种使用语法:

  • typeid(type)

  • typeid(expression)

type_info类提供了public的虚析构函数,但默认构造函数和拷贝构造函数、赋值操作符都是private的,所以type_info对象不能定义或复制,创建type_info对象的唯一方法是调用type_id操作符。若把typeid看作函数,则它是type_info类的友元函数。

type_info还提供以下4种操作:

  • operator ==(): 如果两个对象类型相同返回true,否则返回false

  • operator !=(): 如果两个对象类型不同返回true,否则返回false

  • name(): 返回类型的字符串,类型名字遵循name mangling规则

  • t1.before(t2): 判断t1是否出现在t2之前

只有基类中有虚函数时,编译器才会对typeid中的表达式求值;如果基类中不含虚函数,则typeid返回表达式的静态类型(定义时的类型),编译器就无需对表达式求值。

typeid的操作数是以下情况时,返回编译时类型,即静态类型:

  • 类型名
1std::cout << typeid(int) << std::endl; //i
  • 一个基本类型的变量
1int foo = 10;
2std::cout << typeid(foo) << std::endl; //i
  • 一个具体的对象
1MyClass foo;
2std::cout << typeid(foo) << std::endl; //7MyClass
  • 一个指向不含虚函数的类的对象指针的解引用
1MyClass* pfoo = new MyClass(); //P7MyClass
2std::cout << typeid(*pfoo) << std::endl; //7MyClass
  • 一个指向不含虚函数的类的对象的引用
1MyClass foo;
2const MyClass& rfoo = foo;
3std::cout << typeid(rfoo) << std::endl; //7MyClass

typeid的操作数是以下情况时,返回运行时类型,即动态类型:

  • 一个指向含虚函数的类的对象指针的解引用
 1class Base {
 2public:
 3    virtual ~Base() {}
 4};
 5
 6class Derived : public Base {};
 7
 8...
 9
10Base* b = new Derived();
11
12std::cout << typeid(b).name() << std::endl; //P4Base
13std::cout << typeid(*b).name() << std::endl; //7Derived
  • 一个指向含虚函数的类的对象的引用
1Derived d;
2const Derived& rd = d;
3
4const auto& type_info = typeid(rd);
5std::cout << type_info.name() << std::endl; //7Derived

回顾《对象内存模型》一文中描绘的内存结构布局图,我们以一个虚拟继承的内存布局图为例子

虚拟继承内存布局

在每个虚表指针指向的虚表空间中,虚表空间索引为-1的slot就是一个指向type_info对象地址的指针。多重继承和虚拟继承有多个虚表指针和多个虚表空间(虚表空间在内存中是连续的),每个虚表空间索引为-1的slot都指向同一个type_info对象,所以即使用不同的父类指针,用typeid操作符获取的实际类型都是一致的。

dynamic_cast操作符

我们知道,一个派生类指针或引用强制转换成基类指针或引用是安全的,因为从对象内存布局来看,派生类对象的内存空间总是包含基类的子对象;而反之不安全。把一个基类指针或引用转换为派生类的指针或引用称为向下转型(downcast)。dynamic_cast操作符就是用来安全而有效地进行downcast。

dynamic_cast的语法为

1dynamic_cast<T>(expr)

注意其中expr一定是一个指针或引用。

等价于以下步骤:

  1. 计算expr这个指针或引用所指向的对象的type_info信息:通过虚表指针指向的虚表空间索引为-1的指针获得type_info对象,相当于typeid(expr)

  2. 静态推导目标类型Ttype_info信息:相当于typeid(T)

  3. 比较1和2中两个type_info的大小:若2中的类型信息与1中的类型信息相等,或者2是1的基类,则返回expr对象内存空间中的基类子对象或其地址,否则返回nullptr(指针)或者抛出bad_cast异常(引用)

以上3步如果转换不成功,还需要继续遍历继承链,继续查看是否有转换成功的可能。当继承层次很深的情况下,dynamic_cast是有性能问题的。

什么是反射?

说到RTTI,不得不提一嘴“反射”,RTTI经常被拿来和反射做对比。维基百科是这样定义“反射”的:

在计算机学中,反射式编程(英语:reflective programming)或反射(英语:reflection),是指计算机程序在运行时(runtime)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。

要注意术语“反射”和“内省”(type introspection)的关系。内省(或称“自省”)机制仅指程序在运行时对自身信息(称为元数据)的检测;反射机制不仅包括要能在运行时对程序自身信息进行检测,还要求程序能进一步根据这些信息改变程序状态或结构。

翻译过来就是,反射是程序在运行过程中检查和修改进程本身的功能,支持在运行时动态获取对象信息以及调用对象方法。对象的类型信息一般也叫元数据(metadata)。反射就是操作元数据的过程,进一步拆解反射的步骤:

  • 生成元数据

反射依赖编译器为类生成元数据,C#、Java等语言的编译器会默认生成类型的元数据,而C++编译器几乎抹掉了所有的类型信息,C++默认只为多态类生成元数据。这就决定了C++从语言标准层面不支持反射,只能通过自定义框架来实现和保存元数据。

  • 查询元数据

根据类名或方法名去反射元数据的本质,就是查询,即匹配和搜索字符串。

  • 根据元数据执行对应的操作

根据元数据执行对应的操作,对C++而言非常简单,只需要通过一个函数指针强制转换一下类型就可以做到,而C#、Java等语言就没办法这么做,还得通过反射机制来动态调用。

反射不是C++标准的特性,根本原因在于C++的设计哲学一直是追求零开销,不为用不到的特性付出代价。C#、Java等语言下的反射会在编译时登记一切类型的元数据,包括全部字段信息、全部方法信息、全部接口信息,不管使不使用。这种有备无患的做法向来为C++所不齿,C++只会在必要的时候(比如含虚函数的类型),编译器才会生成必要的type_info信息,而且type_info里的信息非常少,除了一个类型名几乎什么也没有。这种做法在C++里只是RTTI,还远远不能满足反射的条件。

C++的RTTI的确可以提供一部分反射的能力,即查询元数据,但是功能非常单一,没办法根据类型名动态地创建对象和调用对象方法。所以说C++在语言标准层面没法完备支持反射。

当然有很多非标准C++库是可以自己实现完备的反射机制的,比如protobufUE4QT等。

现在有提议为在C++标准中加入编译期反射,即静态反射。不过按照C++标准委员会一贯的谨慎作风,什么时候正式进入标准还有很长的道路要走。

Prev Post: 『C++对象内存模型』
Next Post: 『C++位域』