0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

C++:从技术实现角度聊聊RTTI

CPP开发者 来源:CPP开发者 2023-01-09 13:54 次阅读

第一次接触RTTI,是在<<深度探索c++对象模型>>这本书中,当时对这块的理解比较浅,可能因为知识积累不足吧。后面在工作中用到的越来越多,也逐渐加深了对其认识,但一直没有一个系统的认知,所以抽出一段时间,把这块内容整理下。

背景

RTTI的英文全称是"Runtime Type Identification",中文称为"运行时类型识别",它指的是程序在运行的时候才确定需要用到的对象是什么类型的。用于在运行时(而不是编译时)获取有关对象的信息

C++中,由于存在多态行为,基类指针或者引用指向一个派生类,而其指向的真正类型,在编译阶段是无法知道的:

Base*b=newDerived;
Base&b1=*b;

在上述代码中,如果想知道b的具体类型,只能通过其他方式,而RTTI正是为了解决此问题而诞生,也就是说在运行时,RTTI可以通过特有的方式来告诉调用方其所调用的对象具体信息,一般有如下几种:

  • typeid操作符

  • type_info

  • dynamic_cast操作符

typeid 和 type_info

typeid是C++的关键字之一,等同于sizeof这类的操作符。用来获取类型、变量、表达式的类型信息,适用于C++基础类型、内置类、用户自定义类、模板类等。有如下两种形式:

  • typeid(type)

  • typeid(expr)

用法如下:

#include
#include
#include

classBase{
public:
virtualfloatf(){
return1.0;
}

virtual~Base(){}
};

classDerived:publicBase{
};

intmain(){
Base*p=newDerived;
Base&r=*p;
assert(typeid(p)==typeid(Base*));
assert(typeid(p)!=typeid(Derived*));
assert(typeid(r.f())==typeid(float));

constchar*name=typeid(p).name();

std::cout<< name << std::endl;
  return0;
}

返回值

在上面的例子中,用到了了typeid(xxx).name(),通过其名称可以看出name()函数返回的是具体类型的变量名称(以字符串的方式),那么typeid()的类型又是什么?

在翻阅了cppreference之后了解到,typeid操作符的结果是名为type_info的标准库类型的对象的引用(在头文件中定义),或者说typeid表达式的类型是const std::type_info&

ISO C++标准并没有对type_info有明确的要求,仅仅要求必须有以下几个行为接口

  • • t1 == t2 // 如果两个对象t1和t2类型相同,则返回true;否则返回false

  • • t1 != t2 // 如果两个对象t1和t2类型不同,则返回true;否则返回false

  • •t.name() // 返回类型的C-style字符串

  • •t1.before(t2) // 抱歉,我没用过

正是因为标准对type_info做了有限的规定,这就使得每个编译器厂商对type_info类的实现均不相同,从而使得函数功能也不尽相同。以常用的函数typeid().name()举例,int和Base(自定义类)在VS下输出分别为int和Base,而在gcc编译器下,其输出为i和4Base,又比如typeid(std::vector).name()在gcc下输出为St6vectorIiSaIiEE,这是因为编译期对名称进行了mangle,如果我们想得到跟VS下一样结果的话,可以采用如下方式:

#include
#include
#include
#include
#include
#include

std::stringdemangle(constchar*name){
intstatus=-4;
std::unique_ptr<char,void(*)(void*)>res{
abi::__cxa_demangle(name,NULL,NULL,&status),
std::free
};
return(status==0)?res.get():name;
}

intmain(){
std::vector<int>v;
std::cout<< "before:"<< typeid(v).name()<< "after:"<< demangle(typeid(v).name())<< std::endl;
  return0;
}

输出如下:

before:St6vectorIiSaIiEEafter:std::vector<int,std::allocator<int>>

下面是gcc编译器对type_info类的定义(仅抽取了声明部分),如果有兴趣的读者可以点击链接自行阅读:

classtype_info{
public:
virtual~type_info();
constchar*name()const;
boolbefore(consttype_info&__arg)const;
booloperator==(consttype_info&__arg)const;
boolbefore(consttype_info&__arg)const;
booloperator==(consttype_info&__arg)const;
boolbefore(consttype_info&__arg)const;
booloperator==(consttype_info&__arg)const;
booloperator!=(consttype_info&__arg)const;
size_thash_code()constthrow();
virtualbool__is_pointer_p()const;
virtualbool__is_function_p()const;
virtualbool__do_catch(consttype_info*__thr_type,void**__thr_obj,
unsigned__outer)const;
virtualbool__do_upcast(const__cxxabiv1::__class_type_info*__target,
void**__obj_ptr)const;
protected:
constchar*__name;
explicittype_info(constchar*__n):__name(__n){}
private:
type_info&operator=(consttype_info&);
type_info(consttype_info&);
};

从上述定义可以看出,其析构函数声明为virtual,至少可以说明其存在子对象,那么子对象又是如何被使用的呢?

其实,type_info可以当做一个接口类(通过调用typeid()获取type_info对象,实际上返回的是一个指向子类对象的type_info引用),其有多个子类,对于有虚函数的类来说,在虚函数表中有一个slot专门用来存储该对象的信息,这块内容在文章后面将有详细说明。

实现

在前面有提到,typeid()会返回一个const std::type_info&对象,其中存储这对象的基本信息,那么如果其类型对象为多态和非多态时候,其又有什么区别呢?

如果类型对象至少包含一个虚函数,那么typeid操作符的类型是运行时的事情,也就是说在运行时才能获取到其真正的类型信息;否则,在编译期就能获取其具体类型,甚至在某些情况下,可以对typeid()的结果直接进行替换。

多态

多态,我们知道经常用于运行时,也就是说在运行时刻才会知道其指针或者引用指向的具体类型,如果要对一个包含虚函数的对象获取其类型信息(typeid),那么也是在运行时才能具体知道,举例如下:

#include
#include

classBase
{
public:
virtualvoidfun(){}
};

classDerived:publicBase
{
public:
voidfun(){}
};

voidfun(Base*b){
conststd::type_info&info=typeid(b);
}

intmain(){
Base*b=newDerived;
fun(b);

return0;
}

上述代码汇编后(只取了部分关键代码),如下所示:

fun(Base*):
pushrbp
movrbp,rsp
movQWORDPTR[rbp-24],rdi
movQWORDPTR[rbp-8],OFFSETFLAT:typeinfoforBase*
poprbp
ret
vtableforDerived:
.quad0
.quadtypeinfoforDerived
.quadDerived::fun()
vtableforBase:
.quad0
.quadtypeinfoforBase
.quadBase::fun()
typeinfonameforBase*:
.string"P4Base"
typeinfoforBase*:
.quadvtablefor__cxxabiv1::__pointer_type_info+16
.quadtypeinfonameforBase*
.long0
.zero4
.quadtypeinfoforBase
typeinfonameforDerived:
.string"7Derived"
typeinfoforDerived:
.quadvtablefor__cxxabiv1::__si_class_type_info+16
.quadtypeinfonameforDerived
.quadtypeinfoforBase
typeinfonameforBase:
.string"4Base"
typeinfoforBase:
.quadvtablefor__cxxabiv1::__class_type_info+16
.quadtypeinfonameforBase

首先,我们看fun()函数的汇编(fun(Base*):处),在其中有一行OFFSET FLAT:typeinfo for Base*代表获取Base指针所指向对象的typeinfo。那么typeinfo又是如何获取的呢?

我们以Base指针实际指向Derived对象为例,vtable for Derived:部分代表着Derived类的虚函数表内容,其中有一行typeinfo for Derived代表着Derived类的typeinfo信息,而在该段中有一句typeinfo name for Derived代表着该类的名称(7Derived经过mangle之后,该句在上述代码中可以找到)。

综上内容,可以知道,对于存在虚函数的类来说,其对象的typeinfo信息存储在该类的虚函数表中。在运行时刻,根据指针的实际指向,获取其typeinfo()信息,从而进行相关操作。

其实,不难看出,上述汇编基本列出了类的对象布局,但仍然不是很清晰,gcc提供了一个参数-fdump-class-hierarchy,可以输出类的布局信息,仍然以上述代码为例,其布局信息如下:

VtableforBase
Base:3uentries
0(int(*)(...))0
8(int(*)(...))(&_ZTI4Base)
16(int(*)(...))Base::fun

ClassBase
size=8align=8
basesize=8basealign=8
Base(0x0x7f59773402a0)0nearly-empty
vptr=((&Base::_ZTV4Base)+16u)

VtableforDerived
Derived:3uentries
0(int(*)(...))0
8(int(*)(...))(&_ZTI7Derived)
16(int(*)(...))Derived::fun

ClassDerived
size=8align=8
basesize=8basealign=8
Derived(0x0x7f59773756e8)0nearly-empty
vptr=((&Derived::_ZTV7Derived)+16u)
Base(0x0x7f5977340300)0nearly-empty
primary-forDerived(0x0x7f59773756e8)

我们注意查看,以_ZTI开头的代表类型信息,也就是Type Info的意思(至于以_Z的意思嘛,我理解的是编译器的行为),那么_ZTI7Derived前面的_ZTI代表类型信息,而后面7代表类名(Derived)的长度,最后面的代表类名。通过上面内存布局信息可以看出,在虚函数表中存在一项_ZTI7Derived,其中存储着该对类的类型信息。

如果想要知道其具体名称,可以使用c++filt来查看,如下:

c++filt_ZTI7Derived
typeinfoforDerived

非多态

代码如下:

#include
#include
#include

classMyClss{

};

intmain(){
MyClsss;
conststd::type_info&info=typeid(s);

return0;
}

在上述代码中,实现了一个空类MyClass,然后在main()中,获取该类对象的typeinfo,上述代码汇编如下:

main:
pushrbp
movrbp,rsp
movQWORDPTR[rbp-8],OFFSETFLAT:typeinfoforMyClss
moveax,0
poprbp
ret
typeinfonameforMyClss:
.string"6MyClss"
typeinfoforMyClss:
.quadvtablefor__cxxabiv1::__class_type_info+16
.quadtypeinfonameforMyClss

我们注意下在源码中的第三行即const std::type_info &info = typeid(s);对应汇编的第三行即QWORD PTR [rbp-8], OFFSET FLAT:typeinfo for MyClss,从而可以看出,在编译期,编译器已经知道了对象的具体信息,进而可以在某些情况下,直接由编译器进行替换(比如typeinf().name()操作等)。

dynamic_cast

记得在几年前的一次面试中,面试官提了个问题,对于dynamic_cast,如果操作失败了会有什么行为?当时对这块理解的也不深,所以仅仅回答了:对于指针类型转换,如果失败,则返回NULL,而对于引用,转换失败就抛出bad_cast。

作为C++开发人员,基本都知道dynamic_cast是C++中几个常用的类型转换符之一,其通过类型信息(typeinfo)进行相对安全的类型转换,在转换时,会检查转换的src对象是否真的可以转换成dst类型。dynamic_cast转换符只能用于含有虚函数的类,因此其常常用于运行期,对于不包括虚函数的类,完全可以使用其它几个转换符在编译期进行转换。通常来说,其类型转换分为向上转换和向下转换两种,如下图所示:


4ba688d2-8fd4-11ed-bfe3-dac502259ad0.png

实例代码如下:

#include
#include

classBase1{
public:
voidf0(){}
virtualvoidf1(){}
inta;
};

classBase2{
public:
virtualvoidf2(){}
intb;
};

classDerived:publicBase1,publicBase2{
public:
voidd(){}
voidf2(){}//overrideBase2::f2()
intc;
};

intmain(){
Derived*d=newDerived;
Base1*b1=newDerived;
Base2*b2=dynamic_cast(d);//upcasting向上转换
Derived*d1=dynamic_cast(b1);//downcasting向下转换

return0;
}

实现

通过查阅资料,发现dynamic_cast最终会调用libstdc++中的__dynamic_cast函数,所以曾经以为__dynamic_cast函数就是dynamic_cast的实现版本,但是通过对比参数,发现并非如此:

dynamic_cast(t);//只有一个参数

//__dynamic_cast声明
__dynamic_cast(constvoid*src_ptr,//objectstartedfrom
const__class_type_info*src_type,//typeofthestartingobject
const__class_type_info*dst_type,//desiredtargettype
ptrdiff_tsrc2dst)//howsrcanddstarerelated

所以,有没有可能__dynamic_cast只是dynamic_cast的一个分支实现?

为了验证猜测,示例如下:

#include
#include

classBase1{
public:
voidf0(){}
virtualvoidf1(){}
inta;
};

classBase2{
public:
virtualvoidf2(){}
intb;
};

classDerived:publicBase1,publicBase2{
public:
voidd(){}
voidf2(){}//overrideBase2::f2()
intc;
};

template<classT>
intCheckType(Tt){
intn=0;
if(dynamic_cast(t)){
n|=1;
}
if(dynamic_cast(t)){
n|=2;
}
if(dynamic_cast(t)){
n|=4;
}
returnn;
}

intmain(){
Derived*d=newDerived;
Base1*b1=newBase1;
Base2*b2=newBase2;
CheckType(d);
CheckType(b1);
CheckType(b2);
return0;
}

既然本节内容是dynamic_cast,而只在CheckType()函数中才有对dynamic_cast的调用,那么我们着重分析CheckType函数。

首先,我们通过g++的命令-fdump-class-hierarchy获取其内存布局,Derived内存布局如下(需要注意32 (int (*)(...))-16Base2 (0x0x7f7fbbe5b6c0) 16部分):

VtableforDerived
Derived:7uentries
0(int(*)(...))0
8(int(*)(...))(&_ZTI7Derived)
16(int(*)(...))Base1::f1
24(int(*)(...))Derived::f2
32(int(*)(...))-16
40(int(*)(...))(&_ZTI7Derived)
48(int(*)(...))Derived::_ZThn16_N7Derived2f2Ev

ClassDerived
size=32align=8
basesize=32basealign=8
Derived(0x0x7f7fbbf10c40)0
vptr=((&Derived::_ZTV7Derived)+16u)
Base1(0x0x7f7fbbe5b660)0
primary-forDerived(0x0x7f7fbbf10c40)
Base2(0x0x7f7fbbe5b6c0)16
vptr=((&Derived::_ZTV7Derived)+48u)

向上转换

在CheckType(Derived*)处,通过gdb进行分析,如下:

(gdb)disas
Dumpofassemblercodeforfunction_Z9CheckTypeIP7DerivedEiT_:
0x00000000004009ce<+0>:push%rbp
0x00000000004009cf<+1>:mov%rsp,%rbp
0x00000000004009d2<+4>:mov%rdi,-0x18(%rbp)
=>0x00000000004009d6<+8>:movl$0x0,-0x4(%rbp)

0x00000000004009dd<+15>:cmpq$0x0,-0x18(%rbp)
0x00000000004009e2<+20>:je0x4009e8<_Z9CheckTypeIP7DerivedEiT_+26>
0x00000000004009e4<+22>:orl$0x1,-0x4(%rbp);ift!=nullptr

0x00000000004009e8<+26>:cmpq$0x0,-0x18(%rbp)
0x00000000004009ed<+31>:je0x4009f3<_Z9CheckTypeIP7DerivedEiT_+37>
0x00000000004009ef<+33>:orl$0x2,-0x4(%rbp);ift!=nullptr

0x00000000004009f3<+37>:cmpq$0x0,-0x18(%rbp)
0x00000000004009f8<+42>:je0x400a0b<_Z9CheckTypeIP7DerivedEiT_+61>
0x00000000004009fa<+44>:mov-0x18(%rbp),%rax
0x00000000004009fe<+48>:add$0x10,%rax
0x0000000000400a02<+52>:test%rax,%rax
0x0000000000400a05<+55>:je0x400a0b<_Z9CheckTypeIP7DerivedEiT_+61>
0x0000000000400a07<+57>:orl$0x4,-0x4(%rbp);ift!=nullptr&&t+0x10!=nullptr
0x0000000000400a0b<+61>:mov-0x4(%rbp),%eax
0x0000000000400a0e<+64>:pop%rbp
0x0000000000400a0f<+65>:retq
Endofassemblerdump.

为了便于理解,在上述代码关键部分加上了注释.

我们注意到,在上述汇编代码中,没有找到外部函数调用(__dynamic_cast),而仅仅是一些常用的跳转和比较指令。其中,前两条orl指令的执行条件为t不为0,而第三条orl指令的执行条件为t不为0且t+16不为0。这几个行为是在编译期完成的,也就是说在本例中,dynamic_cast由编译器在编译期实现了转换,所以可以说其是静态转换

在前面的内存布局中,Derived对象有3个偏移量,分别为(Derived/Base1 = 0, Base2 = +0x10),即相对于Derived和Base1其偏移量为0,而相对于Base2其偏移量为16。前两个dynamic_cast是Derived* -> Derived* 和 Derived* -> Base1*,都不需要调整指针,所以在CheckType的if语句中使用t的值作为dynamic_cast的返回值。在第三次Derived* -> Base2*转换中,编译时知道地址是t+0x10,所以计算t+0x10的结果就是dynamic_cast的返回值。

至此,我们可以说,dynamic_cast操作中,向上转换是静态操作,在编译阶段完成

向下转换

在CheckType(Base1*)处,通过gdb进行分析,如下:

(gdb)disas
Dumpofassemblercodeforfunction_Z9CheckTypeIP5Base1EiT_:
0x0000000000400a10<+0>:push%rbp
0x0000000000400a11<+1>:mov%rsp,%rbp
0x0000000000400a14<+4>:sub$0x20,%rsp
0x0000000000400a18<+8>:mov%rdi,-0x18(%rbp)
=>0x0000000000400a1c<+12>:movl$0x0,-0x4(%rbp)

0x0000000000400a23<+19>:mov-0x18(%rbp),%rax
0x0000000000400a27<+23>:test%rax,%rax
0x0000000000400a2a<+26>:je0x400a4f<_Z9CheckTypeIP5Base1EiT_+63>
0x0000000000400a2c<+28>:mov$0x0,%ecx;src2dst=0
0x0000000000400a31<+33>:mov$0x400c98,%edx;dst_type<_ZTV7Derived>
0x0000000000400a36<+38>:mov$0x400cf8,%esi;src_type<_ZTI5Base1>
0x0000000000400a3b<+43>:mov%rax,%rdi
0x0000000000400a3e<+46>:callq0x4006d0<__dynamic_cast@plt>
0x0000000000400a43<+51>:test%rax,%rax
0x0000000000400a46<+54>:je0x400a4f<_Z9CheckTypeIP5Base1EiT_+63>
0x0000000000400a48<+56>:mov$0x1,%eax
0x0000000000400a4d<+61>:jmp0x400a54<_Z9CheckTypeIP5Base1EiT_+68>
0x0000000000400a4f<+63>:mov$0x0,%eax
0x0000000000400a54<+68>:test%al,%al
0x0000000000400a56<+70>:je0x400a5c<_Z9CheckTypeIP5Base1EiT_+76>
0x0000000000400a58<+72>:orl$0x1,-0x4(%rbp)

0x0000000000400a5c<+76>:cmpq$0x0,-0x18(%rbp)
0x0000000000400a61<+81>:je0x400a67<_Z9CheckTypeIP5Base1EiT_+87>
0x0000000000400a63<+83>:orl$0x2,-0x4(%rbp)

0x0000000000400a67<+87>:mov-0x18(%rbp),%rax
0x0000000000400a6b<+91>:test%rax,%rax
0x0000000000400a6e<+94>:je0x400a95<_Z9CheckTypeIP5Base1EiT_+133>
0x0000000000400a70<+96>:mov$0xfffffffffffffffe,%rcx;src2dst=-2
0x0000000000400a77<+103>:mov$0x400ce0,%edx;dst_type<_ZTI5Base2>
0x0000000000400a7c<+108>:mov$0x400cf8,%esi;src_type<_ZTI5Base1>
0x0000000000400a81<+113>:mov%rax,%rdi
0x0000000000400a84<+116>:callq0x4006d0<__dynamic_cast@plt>
0x0000000000400a89<+121>:test%rax,%rax
0x0000000000400a8c<+124>:je0x400a95<_Z9CheckTypeIP5Base1EiT_+133>
0x0000000000400a8e<+126>:mov$0x1,%eax
0x0000000000400a93<+131>:jmp0x400a9a<_Z9CheckTypeIP5Base1EiT_+138>

0x0000000000400a95<+133>:mov$0x0,%eax
0x0000000000400a9a<+138>:test%al,%al
0x0000000000400a9c<+140>:je0x400aa2<_Z9CheckTypeIP5Base1EiT_+146>
0x0000000000400a9e<+142>:orl$0x4,-0x4(%rbp)

0x0000000000400aa2<+146>:mov-0x4(%rbp),%eax
0x0000000000400aa5<+149>:leaveq
---Typetocontinue,orqtoquit---
0x0000000000400aa6<+150>:retq
Endofassemblerdump.

通过上述汇编代码,很明显可以看出,Base1* -> Base1*不进行任何转换(这不废话嘛,类型是相同的)。而对于Base1* -> Derived* 以及 Base1* -> Base2* 则需要调用__dynamic_cast函数,而其所需要的参数,在汇编指令中也可以看出,下面将对该函数进行详细分析。

__dynamic_cast参数语义

声明如下:

__dynamic_cast(constvoid*src_ptr,//objectstartedfrom
const__class_type_info*src_type,//typeofthestartingobject
const__class_type_info*dst_type,//desiredtargettype
ptrdiff_tsrc2dst)//howsrcanddstarerelated

在上述声明中:

  • •src_ptr代表需要转换的指针

  • •src_type原始类型

  • •dst_type目标类型

  • •src2dst表示从dst到src的偏移量,当该值为如下3个之一时候,有特殊含义:

    • •-1: no hint

    • •-2: src is not a public base of dst

    • •-3: src is a multiple public base type but never a virtual base type

src2dst的值中,-2代表src 不是 dst 的公共基类,如上节中的Base1* -> Base2*;-3代表src是多个(dst的)公共基类并且不是虚基类,即没有虚拟继承的菱形继承。如果不为-1 -2 -3三值之一,则src2dst代表src和dst的偏移,如上一节中从Base1* -> Base1*转换的时候传值为0,即偏移为0;Base1*->Base2*转换的时候,传的值为-2(0xfffffffffffffffe)。

__dynamic_cast实现

extern"C"void*
__dynamic_cast(constvoid*src_ptr,//objectstartedfrom
const__class_type_info*src_type,//typeofthestartingobject
const__class_type_info*dst_type,//desiredtargettype
ptrdiff_tsrc2dst)//howsrcanddstarerelated
{
constvoid*vtable=*static_cast<constvoid*const*>(src_ptr);
constvtable_prefix*prefix=
adjust_pointer(vtable,
-offsetof(vtable_prefix,origin));
constvoid*whole_ptr=
adjust_pointer<void>(src_ptr,prefix->whole_object);
const__class_type_info*whole_type=prefix->whole_type;
__class_type_info::__dyncast_resultresult;

//Ifthewholeobjectvptrdoesn'trefertothewholeobjecttype,we're
//inthemiddleofconstructingaprimarybase,andsrcisaseparate
//base.Thishasundefinedbehaviorandwecan'tfindanythingoutside
//ofthebasewe'reactuallyconstructing,sofailnowratherthan
//segfaultlatertryingtouseavbaseoffsetthatdoesn'texist.
constvoid*whole_vtable=*static_cast<constvoid*const*>(whole_ptr);
constvtable_prefix*whole_prefix=
adjust_pointer(whole_vtable,
-offsetof(vtable_prefix,origin));
constvoid*whole_vtable=*static_cast<constvoid*const*>(whole_ptr);
constvtable_prefix*whole_prefix=
(adjust_pointer
(whole_vtable,-ptrdiff_t(offsetof(vtable_prefix,origin))));
if(whole_prefix->whole_type!=whole_type)
returnNULL;

//Avoidvirtualfunctioncallinthesimplesuccesscase.
if(src2dst>=0
&&src2dst==-prefix->whole_object
&&*whole_type==*dst_type)
returnconst_cast<void*>(whole_ptr);

whole_type->__do_dyncast(src2dst,__class_type_info::__contained_public,
dst_type,whole_ptr,src_type,src_ptr,result);
...

这个函数先通过src_ptr来初始化部分局部变量:

  • vtable通过对src_ptr解引用(deref)获取

  • vtable_prefix子对象虚函数表地址,通过vtable的类型信息和offset_to_top来获取

  • whole_ptrsrc_ptr最底层的派生类地址,一般为src_ptr的值加上offset_to_top

  • whole_typesrc_ptr最底层的派生类的虚函数表中的类型信息(type info)

  • whole_vtablewhole对象的虚函数表地址

然后调用whole_type->__do_dyncast,而这也是该函数的核心模块。然后根据返回值的内容来判断结果,并进行相应的操作。

其中,vtable_prefix的定义如下:

structvtable_prefix
{
//Offsettomostderivedobject.
ptrdiff_twhole_object;
//Pointertomostderivedtype_info.
const__class_type_info*whole_type;
//Whataclass'svptrpointsto.
constvoid*origin;
};
  • •whole_object 表示当前指针指向对象的偏移量

  • • whole_type 指向 C++ 对象的类型:class(基类)、si_class(单一继承类型)、vmi_class(多重或虚拟继承类型)

  • • origin 表示虚函数表的入口,等于实例的虚指针。origin在这里的作用是offsetof,反向获取whole_object的指针。

__class_type_info::__dyncast_result 定义如下:

struct__class_type_info::__dyncast_result
{
constvoid*dst_ptr;//pointertotargetobjectorNULL
__sub_kindwhole2dst;//pathfrommostderivedobjecttotarget
__sub_kindwhole2src;//pathfrommostderivedobjecttosubobject
__sub_kinddst2src;//pathfromtargettosubobject
intwhole_details;//detailsofthewholeclasshierarchy
...

在前面提到,__do_dyncast被调用之后,后面就根据其出参result的返回值进行各种判断,那么result到底什么意思呢?其实,从上述定义就能看出,whole2dst代表whole对象向dst的转换结果,而whole2src代表whole对象向src的转换结果等,通过下面的图能更加清晰的理解转换过程:

4bc9c7f2-8fd4-11ed-bfe3-dac502259ad0.png

在上图中,有3中类型,src、whole以及dst,__do_dyncast函数功能则是提供该3中类型的转换结果,在只有满足以下3中情况时候,__dynamic_cast才返回非空:

  • •src是dst的公共基类

  • •dst和src不是直接继承的关系,但是whole2src和whole2dst都是public

  • •dst2src未知且whole2src是非public虚继承关系,则不使用whole,重新获取dst和src的关系

这块逻辑比较绕,其实可以将关系理解为图上的一条条连接线,节点理解为类型信息,dynamic_cast的过程,就是判断有没有从src到dst有没有路径的过程。

继承关系

在前面的内容中,遇到过vtable for __cxxabiv1::__si_class_type_info+16这种,那么si_class_type_info又是什么呢?同样,在翻阅了源码之后,发现其是gcc中继承关系的一种。

在gcc中,将继承关系表示为图结构,对于类,有以下三种类型(type info):

  • •class __class_type_info : public std::type_info

  • •class __si_class_type_info : public __class_type_info

  • •class __vmi_class_type_info : public __class_type_info

其中,__class_type_info 表示没有继承关系的类,__si_class_type_info 表示单继承的类,__vmi_class_type_info 表示多继承或虚拟继承的类。类名开头的si代表单继承,vmi代表虚拟或多重继承。

查看定义,__si_class_type_info 包含指向基类类型的单个指针,而 __vmi_class_type_info 包含指向基类类型的指针数组。基类类型存储其子对象的位置和基类的类型(public、virtual)。

仍然以上一节中的代码为例,使用gdb来分析__ZTI7Derived、__ZTI5Base1、__ZTI5Base2的关系

(gdb)x/2xg&_ZTI7Derived
0x555555755d80<_ZTI7Derived>:0x00007ffff7dca5d80x0000555555554d74
(gdb)x/2xg0x00007ffff7dca5d8
0x7ffff7dca5d8<_ZTVN10__cxxabiv121__vmi_class_type_infoE+16>:0x00007ffff7ae09200x00007ffff7ae0940

(gdb)p*(__cxxabiv1::__vmi_class_type_info*)0x555555755d80
$2={
<__cxxabiv1::__class_type_info>={
={
_vptr.type_info=0x7ffff7dca5d8,
__name=0x555555554d74"7Derived"
},},
membersof__cxxabiv1:
__flags=0,
__base_count=2,
__base_info={{
__base_type=0x555555755dc8,
__offset_flags=2
}}

(gdb)p(*(__cxxabiv1::__vmi_class_type_info*)0x555555755d80)->__base_info[0]
$4={
__base_type=0x555555755dc8,
__offset_flags=2<---- __public_mask(2) | offset:0x00
}
(gdb) p (*(__cxxabiv1::__vmi_class_type_info*)0x555555755d80)->__base_info[1]
$5={
__base_type=0x555555755db8,
__offset_flags=4098<---- __public_mask(2) | offset:0x10
}

(gdb) x/2xg 0x555555755dc8
0x555555755dc8 <_ZTI5Base1>:0x00007ffff7dc98d80x0000555555554d7b
(gdb)x/2xg0x00007ffff7dc98d8
0x7ffff7dc98d8<_ZTVN10__cxxabiv117__class_type_infoE+16>:0x00007ffff7add9300x00007ffff7add950

(gdb)x/2xg0x555555755db8
0x555555755db8<_ZTI5Base2>:0x00007ffff7dc98d80x0000555555554d77
(gdb)x/2xg0x00007ffff7dc98d8
0x7ffff7dc98d8<_ZTVN10__cxxabiv117__class_type_infoE+16>:0x00007ffff7add9300x00007ffff7add950

(gdb)p*(__cxxabiv1::__class_type_info*)0x555555755dc8
$6={
={
_vptr.type_info=0x7ffff7dc98d8,
__name=0x555555554d7b"5Base1"
},}

(gdb)p*(__cxxabiv1::__class_type_info*)0x555555755db8
$7={
={
_vptr.type_info=0x7ffff7dc98d8,
__name=0x555555554d77"5Base2"
},}

通过上述代码,可以看出_ZTI7Derived是__vmi_class_type_info的一个实例,其基类数组的类型分别是_ZTI5Base1和_ZTI5Base2,通过将这些类型展开,就能获取一张图结构,进而说明dynamic_cast的过程就是遍历图结构确定路径关系的过程,采用的是深度优先搜索

审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • C++
    C++
    +关注

    关注

    22

    文章

    2108

    浏览量

    73655
  • 代码
    +关注

    关注

    30

    文章

    4788

    浏览量

    68622
  • 编译
    +关注

    关注

    0

    文章

    657

    浏览量

    32873

原文标题:C++:从技术实现角度聊聊RTTI

文章出处:【微信号:CPP开发者,微信公众号:CPP开发者】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    运动控制卡周期上报实时数据IO状态之C++

    使用C++进行运动控制卡的周期上报功能实现
    的头像 发表于 12-17 13:59 223次阅读
    运动控制卡周期上报实时数据IO状态之<b class='flag-5'>C++</b>篇

    C7000 C/C++优化指南用户手册

    电子发烧友网站提供《C7000 C/C++优化指南用户手册.pdf》资料免费下载
    发表于 11-09 15:00 0次下载
    <b class='flag-5'>C</b>7000 <b class='flag-5'>C</b>/<b class='flag-5'>C++</b>优化指南用户手册

    C语言和C++中结构体的区别

    同样是结构体,看看在C语言和C++中有什么区别?
    的头像 发表于 10-30 15:11 230次阅读

    C7000优化C/C++编译器

    电子发烧友网站提供《C7000优化C/C++编译器.pdf》资料免费下载
    发表于 10-30 09:45 0次下载
    <b class='flag-5'>C</b>7000优化<b class='flag-5'>C</b>/<b class='flag-5'>C++</b>编译器

    ostream在c++中的用法

    ostream 是 C++ 标准库中一个非常重要的类,它位于 头文件中(实际上,更常见的是通过包含 头文件来间接包含 ,因为 包含了 和 )。 ostream 类及其派生类(如 std::cout
    的头像 发表于 09-20 15:11 716次阅读

    OpenVINO2024 C++推理使用技巧

    很多人都使用OpenVINO新版的C++ 或者Python的SDK,都觉得非常好用,OpenVINO2022之后的版本C++ SDK做了大量的优化与整理,已经是非常贴近开发的使用习惯与推理方式。与OpenCV的Mat对象对接方式更是几乎无缝对接,非常的方便好用。
    的头像 发表于 07-26 09:20 912次阅读

    C++语言基础知识

    电子发烧友网站提供《C++语言基础知识.pdf》资料免费下载
    发表于 07-19 10:58 7次下载

    C++实现类似instanceof的方法

    函数,可实际上C++中没有。但是别着急,其实C++中有两种简单的方法可以实现类似Java中的instanceof的功能。 在 C++ 中,确定对象的类型是编程中实际需求,使开发人员
    的头像 发表于 07-18 10:16 592次阅读
    <b class='flag-5'>C++</b>中<b class='flag-5'>实现</b>类似instanceof的方法

    C/C++中两种宏实现方式

    #ifndef的方式受C/C++语言标准支持。它不仅可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件(或者代码片段)不会被不小心同时包含。
    的头像 发表于 04-19 11:50 628次阅读

    鸿蒙OS开发实例:【Native C++

    使用DevEco Studio创建一个Native C++应用。应用采用Native C++模板,实现使用NAPI调用C标准库的功能。使用C
    的头像 发表于 04-14 11:43 2635次阅读
    鸿蒙OS开发实例:【Native <b class='flag-5'>C++</b>】

    使用 MISRA C++:2023® 避免基于范围的 for 循环中的错误

    在前两篇博客中,我们 向您介绍了新的 MISRA C++ 标准 和 C++ 的历史 。在这篇博客中,我们将仔细研究以 C++ 中 for 循环为中心的特定规则。
    的头像 发表于 03-28 13:53 799次阅读
    使用 MISRA <b class='flag-5'>C++</b>:2023® 避免基于范围的 for 循环中的错误

    c语言,c++,java,python区别

    C语言、C++、Java和Python是四种常见的编程语言,各有优点和特点。 C语言: C语言是一种面向过程的编程语言。它具有底层的特性,能够对计算机硬件进行直接操作。
    的头像 发表于 02-05 14:11 2394次阅读

    vb语言和c++语言的区别

    VB语言和C++语言是两种不同的编程语言,虽然它们都属于高级编程语言,但在设计和用途上有很多区别。下面将详细比较VB语言和C++语言的区别。 设计目标: VB语言(Visual Basic)是由
    的头像 发表于 02-01 10:20 2325次阅读

    C++在Linux内核开发中争议到成熟

    Linux 内核邮件列表中一篇已有六年历史的老帖近日再次引发激烈讨论 —— 主题是建议将 Linux 内核的开发语言 C 转换为更现代的 C++
    的头像 发表于 01-31 14:11 633次阅读
    <b class='flag-5'>C++</b>在Linux内核开发中<b class='flag-5'>从</b>争议到成熟

    C++简史:C++是如何开始的

    MISRA C++:2023,MISRA® C++ 标准的下一个版本,来了!为了帮助您做好准备,我们介绍了 Perforce 首席技术支持工程师 Frank van den Beuken 博士撰写
    的头像 发表于 01-11 09:00 598次阅读
    <b class='flag-5'>C++</b>简史:<b class='flag-5'>C++</b>是如何开始的