首页 > 编程语言 >C/C++杂记:运行时类型识别(RTTI)与动态类型转换原理

C/C++杂记:运行时类型识别(RTTI)与动态类型转换原理

时间:2023-05-31 12:57:44浏览次数:57  
标签:类型转换 i8 typeid 对象 C++ std RTTI type

运行时类型识别(RTTI)的引入有三个作用:

  1. 配合typeid操作符的实现;
  2. 实现异常处理中catch的匹配过程;
  3. 实现动态类型转换dynamic_cast。

1. typeid操作符的实现

1.1. 静态类型的情形

C++中支持使用typeid关键字获取对象类型信息,它的返回值类型是const std::type_info&,例:

#include <typeinfo>
#include <cassert>
struct B {} b, c;
struct D : B {} d;
void test() {
    const std::type_info& tb = typeid(b); 
    const std::type_info& tc = typeid(c); 
    const std::type_info& td = typeid(d);
    assert(tb == tc);   // b和c具有相同的类型
    assert(&tb == &tc); // tb和tc引用的是相同的对象
    assert(tb != td);   // 虽然D是B的子类,但是b和d的类型却不同
    assert(&tb != &td); // tb和td引用的是不同的对象
}

理论上讲,编译器会为每一种类型生成一个能唯一标识该类型的类型信息对象,typeid返回的就是该对象的引用。

通过查看clang编译器生成的LLVM汇编程序(LLVM汇编程序比本地汇编程序可读性较强),可以证明这一点。
使用clang编译上述源码:“clang -S -emit-llvm test.cpp -o -”,生成LLVM汇编程序包含以下信息(为了方便阅读,省略了部分无关内容):

@_ZTI1B = linkonce_odr constant { i8*, i8* } { ... }
@_ZTI1D = linkonce_odr constant { i8*, i8*, i8* } { ... }

define void @_Z4testv() #0 {
  %tb = alloca %"class.std::type_info"*, align 8
  %tc = alloca %"class.std::type_info"*, align 8
  %td = alloca %"class.std::type_info"*, align 8
  store bitcast ({ i8*, i8* }* @_ZTI1B to %"class.std::type_info"*), %tb, align 8
  store bitcast ({ i8*, i8* }* @_ZTI1B to %"class.std::type_info"*), %tc, align 8
  store bitcast ({ i8*, i8*, i8* }* @_ZTI1D to %"class.std::type_info"*), %td, align 8
  ...

其中:

  • @_ZTI1B 和@_ZTI1D 是两个全局变量,用以存储std::type_info(或者其子类)对象。
  • 上述LLVM汇编程序中还列出了test()函数的起始部分内容,其中将@_ZTI1B 存储于%tb和%tc,将@_ZTI1D 存储于%td,正好对应原程序中的引用初始化语句。

附加说明:

  • LLVM汇编语言也称之为LLVM中间表示(IR, Intermediate Representation),其中全局变量以“@”开头。详细请参见:LLVM Language Reference Manual。
  • _ZTI1B和_ZTI1D是经过名字修饰(name mangling)修饰之后的变量名,linux下可以使用c++filt命令还原成可读形式(例如:c++filt _ZTI1B输出“typeinfo for B”,说明_ZTI1B是标识B类型的全局变量)。

1.2. 动态类型的情形

当typeid的操作数引用的是一个动态类(含有虚函数的类) 类型时,它的返回值是被引用对象对应类型的类型信息对象,例:

#include <typeinfo>
#include <cassert>
struct B { virtual void foo() {} };
struct C { virtual void bar() {} };
struct D : B, C {};
void test() {
    D d;
    B& rb = d;
    C& rc = d;
    assert(typeid(rb) == typeid(d));  // rb引用的类型与d相同
    assert(typeid(rb) == typeid(rc)); // rb引用的类型与rc引用的类型相同
}

编译时可能还不知道rb或rc引用的类型,运行时怎么能判断该返回基类还是派生类对应的类型信息对象呢?

还记得“C/C++杂记:深入虚表结构”一文中讲过的-fdump-class-hierarchy选项吧,用它将D的虚表打印出来如下:

可见,无论是“主虚表”还是“次虚表”,其中的RTTI信息位置都是&_ZTI1D(即D类型对应的类型信息对象)。

正是利用了这一点,运行时便可以通过vptr找到“虚函数表”,而“虚函数表”之前的一个位置存放了需要的类型信息对象,typeid可以直接返回这里的类型信息对象引用即可。
下面的图示描述了这一过程:

2. 实现异常处理中catch的匹配过程

catch的匹配过程也可利用与typeid相似的原理进行类型匹配判断,此不再赘述。

3. 动态类型转换(dynamic_cast)

说明:本节不考虑虚拟继承的情形。

先上一个例子:

转换过程:
(1) 对#2来说最为简单,首先获取RTTI对象,RTTI对象与目标类型信息对象一致,而偏移值也为0,所以只用返回源地址(pb)即可。
(2) 对#1和#3来说,RTTI对象与目标类型信息对象一致,但是有偏移值-8,所以返回值为“(char*)pa + (-8)”或“(char*)pc + (-8)”。
(3) 对#4来说,RTTI对象与目标类型信息对象不一致,但是目标类型C 是RTTI对象表示类型(D)是基类(后面会讨论如何判断继承关系),因此转换也是可行的。

用clang编译上述源码,生成LLVM汇编程序如下(已作简化):

@_ZTI1A= linkonce_odr constant { i8*, i8* } { ... }
@_ZTI1B= linkonce_odr constant { i8*, i8* } { ... }
@_ZTI1C= linkonce_odr constant { i8*, i8*, i8* } {..., i8* bitcast ({ i8*, i8* }* @_ZTI1A to i8*) }
@_ZTI1D= linkonce_odr constant { i8*, i8*, i32, i32, i8*, i64, i8*, i64 } { ...,
        i8* bitcast ({ i8*, i8* }* @_ZTI1B to i8*), i64 2,
        i8* bitcast ({ i8*, i8*, i8* }* @_ZTI1C to i8*), i64 2050
    }

从中可以看出,RTTI对象中存放的内容还包括基类的RTTI对象指针,成树状结构:

因此继承关系可以通过此树状结构判断,有了继承关系,再递归从虚表中查找基类子对象在派生类中的偏移值,便可以确定最终返回地址。

4. 参考

(1) Itanium C++ ABI

(2) LLVM Language Reference Manual

(3) libc++abi源码(private_typeinfo.h文件

标签:类型转换,i8,typeid,对象,C++,std,RTTI,type
From: https://www.cnblogs.com/tomato-haha/p/17445798.html

相关文章

  • C/C++杂记:虚函数的实现的基本原理
    1.概述简单地说,每一个含有虚函数(无论是其本身的,还是继承而来的)的类都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针。例:其中:B的虚函数表中存放着B::foo和B::bar两个函数指针。D的虚函数表中存放的既有继承自B的虚函数B::foo,又有重写(override)了基......
  • C/C++杂记:深入理解数据成员指针、函数成员指针
    1.数据成员指针对于普通指针变量来说,其值是它所指向的地址,0表示空指针。而对于数据成员指针变量来说,其值是数据成员所在地址相对于对象起始地址的偏移值,空指针用-1表示。例:代码示例:structX{inta;intb;};#defineVALUE_OF_PTR(p)(*(long*)&p)int......
  • 【C++】c++单继承、多继承、菱形继承内存布局(虚函数表结构)
    单继承:只有一个基类和一个派生类classBase{public:virtualvoidfun1(){cout<<"Base::func1()"<<endl;}virtualvoidfun2(){cout<<"Base::func2()"<<endl;}private:intb;......
  • C++ 在函数内部输出当前类名方式
    开发环境:QtCreator C++1usingnamespacestd;23/*基类汽车*/4classCar5{6public:7Car(){}8virtual~Car(){}9virtualvoidmove(void);10};1112/*基本属性汽车运动*/13voidCar::move(void)14{15cout<<......
  • C++多态虚函数表详解(多重继承、多继承情况)
    本文关键词:C++多态多继承多重继承虚函数表虚函数指针动态绑定概述:C++相对其他面向对象语言来说,之所以灵活、高效。很大程度的占比在于其多态技术和模板技术。C++虚函数表是支撑C++多态的重要技术,它是C++动态绑定技术的核心。本文章将着重图解虚函数表相关知识,在阅读本文......
  • c++中的析构函数和纯虚函数
    析构函数:c++中当delete一个类对象时,会默认调用其析构函数,析构函数的作用就是释放对象占用的堆空间。一般基类的析构函数需写成虚函数,这是因为在多态下,我们一般用基类的指针来指向一个子类对象,若基类的虚函数未被重写,那么可能会造成内存泄漏。因此需要在子类重写基类的虚函数来......
  • 【c&c++】erase怎么用c语言,C++ erase()函数使用时的注意点
    遇见的场景删除vector容器指定元素时;erase()函数的用法vector::erase():从指定容器删除指定位置的元素或某段范围内的元素。具体用法如下:iteratorerase(iterator_Where);删除指定位置的元素,返回值是一个迭代器,指向删除元素的下一个元素;iteratorerase(iterator_First,i......
  • BDB c++例子,从源码编译到运行
    第一步先下载源码,解压后./dist/configure--enable-cxx编译,然后make,makeinstall--enable-cxxTobuildtheBerkeleyDBC++API,enter--enable-cxxasanargumenttoconfigure. 默认的安装路径是:/usr/local/BerkeleyDB.6.1/ 代码如下:#include<stdlib.h>#include<strin......
  • MongoDB C++ gridfs worked example
    使用libmongoc,参考:http://mongoc.org/libmongoc/current/mongoc_gridfs_t.html#include<mongoc.h>#include<stdio.h>#include<stdlib.h>#include<fcntl.h>classMongoGridFS{public:MongoGridFS(constchar*db);~MongoGridFS();......
  • mongodb c++ driver安装踩坑记
     安装教程:https://mongodb.github.io/mongo-cxx-driver/mongocxx-v3/installation/(1)“initializer_list”filenotfoundhttp://stackoverflow.com/questions/19493671/initializer-list-no-such-file-or-directorySinceyouareusing GCC-4.8 andyourproblemisthatyoud......