第一次接触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转换符只能用于含有虚函数的类,因此其常常用于运行期,对于不包括虚函数的类,完全可以使用其它几个转换符在编译期进行转换。通常来说,其类型转换分为向上转换和向下转换两种,如下图所示:
实例代码如下:
#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 (*)(...))-16和Base2 (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的转换结果等,通过下面的图能更加清晰的理解转换过程:
在上图中,有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++
+关注
关注
22文章
2108浏览量
73655 -
代码
+关注
关注
30文章
4788浏览量
68622 -
编译
+关注
关注
0文章
657浏览量
32873
原文标题:C++:从技术实现角度聊聊RTTI
文章出处:【微信号:CPP开发者,微信公众号:CPP开发者】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论