0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

C++:從技術(shù)實(shí)現(xiàn)角度聊聊RTTI

CPP開發(fā)者 ? 來源:CPP開發(fā)者 ? 2023-01-09 13:54 ? 次閱讀

第一次接觸RTTI,是在<<深度探索c++對象模型>>這本書中,當(dāng)時(shí)對這塊的理解比較淺,可能因?yàn)橹R積累不足吧。后面在工作中用到的越來越多,也逐漸加深了對其認(rèn)識,但一直沒有一個(gè)系統(tǒng)的認(rèn)知,所以抽出一段時(shí)間,把這塊內(nèi)容整理下。

背景

RTTI的英文全稱是"Runtime Type Identification",中文稱為"運(yùn)行時(shí)類型識別",它指的是程序在運(yùn)行的時(shí)候才確定需要用到的對象是什么類型的。用于在運(yùn)行時(shí)(而不是編譯時(shí))獲取有關(guān)對象的信息。

C++中,由于存在多態(tài)行為,基類指針或者引用指向一個(gè)派生類,而其指向的真正類型,在編譯階段是無法知道的:

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

在上述代碼中,如果想知道b的具體類型,只能通過其他方式,而RTTI正是為了解決此問題而誕生,也就是說在運(yùn)行時(shí),RTTI可以通過特有的方式來告訴調(diào)用方其所調(diào)用的對象具體信息,一般有如下幾種:

  • ?typeid操作符

  • ?type_info

  • ?dynamic_cast操作符

typeid 和 type_info

typeid是C++的關(guān)鍵字之一,等同于sizeof這類的操作符。用來獲取類型、變量、表達(dá)式的類型信息,適用于C++基礎(chǔ)類型、內(nèi)置類、用戶自定義類、模板類等。有如下兩種形式:

  • ?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<return0;
}

返回值

在上面的例子中,用到了了typeid(xxx).name(),通過其名稱可以看出name()函數(shù)返回的是具體類型的變量名稱(以字符串的方式),那么typeid()的類型又是什么?

在翻閱了cppreference之后了解到,typeid操作符的結(jié)果是名為type_info的標(biāo)準(zhǔn)庫類型的對象的引用(在頭文件中定義),或者說typeid表達(dá)式的類型是const std::type_info&。

ISO C++標(biāo)準(zhǔn)并沒有對type_info有明確的要求,僅僅要求必須有以下幾個(gè)行為接口

  • ? t1 == t2 // 如果兩個(gè)對象t1和t2類型相同,則返回true;否則返回false

  • ? t1 != t2 // 如果兩個(gè)對象t1和t2類型不同,則返回true;否則返回false

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

  • ?t1.before(t2) // 抱歉,我沒用過

正是因?yàn)闃?biāo)準(zhǔn)對type_info做了有限的規(guī)定,這就使得每個(gè)編譯器廠商對type_info類的實(shí)現(xiàn)均不相同,從而使得函數(shù)功能也不盡相同。以常用的函數(shù)typeid().name()舉例,int和Base(自定義類)在VS下輸出分別為int和Base,而在gcc編譯器下,其輸出為i和4Base,又比如typeid(std::vector).name()在gcc下輸出為St6vectorIiSaIiEE,這是因?yàn)榫幾g期對名稱進(jìn)行了mangle,如果我們想得到跟VS下一樣結(jié)果的話,可以采用如下方式:

#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())<return0;
}

輸出如下:

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

下面是gcc編譯器對type_info類的定義(僅抽取了聲明部分),如果有興趣的讀者可以點(diǎn)擊鏈接自行閱讀:

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&);
};

從上述定義可以看出,其析構(gòu)函數(shù)聲明為virtual,至少可以說明其存在子對象,那么子對象又是如何被使用的呢?

其實(shí),type_info可以當(dāng)做一個(gè)接口類(通過調(diào)用typeid()獲取type_info對象,實(shí)際上返回的是一個(gè)指向子類對象的type_info引用),其有多個(gè)子類,對于有虛函數(shù)的類來說,在虛函數(shù)表中有一個(gè)slot專門用來存儲(chǔ)該對象的信息,這塊內(nèi)容在文章后面將有詳細(xì)說明。

實(shí)現(xiàn)

在前面有提到,typeid()會(huì)返回一個(gè)const std::type_info&對象,其中存儲(chǔ)這對象的基本信息,那么如果其類型對象為多態(tài)和非多態(tài)時(shí)候,其又有什么區(qū)別呢?

如果類型對象至少包含一個(gè)虛函數(shù),那么typeid操作符的類型是運(yùn)行時(shí)的事情,也就是說在運(yùn)行時(shí)才能獲取到其真正的類型信息;否則,在編譯期就能獲取其具體類型,甚至在某些情況下,可以對typeid()的結(jié)果直接進(jìn)行替換。

多態(tài)

多態(tài),我們知道經(jīng)常用于運(yùn)行時(shí),也就是說在運(yùn)行時(shí)刻才會(huì)知道其指針或者引用指向的具體類型,如果要對一個(gè)包含虛函數(shù)的對象獲取其類型信息(typeid),那么也是在運(yùn)行時(shí)才能具體知道,舉例如下:

#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;
}

上述代碼匯編后(只取了部分關(guān)鍵代碼),如下所示:

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()函數(shù)的匯編(fun(Base*):處),在其中有一行OFFSET FLAT:typeinfo for Base*代表獲取Base指針?biāo)赶驅(qū)ο蟮膖ypeinfo。那么typeinfo又是如何獲取的呢?

我們以Base指針實(shí)際指向Derived對象為例,vtable for Derived:部分代表著Derived類的虛函數(shù)表內(nèi)容,其中有一行typeinfo for Derived代表著Derived類的typeinfo信息,而在該段中有一句typeinfo name for Derived代表著該類的名稱(7Derived經(jīng)過mangle之后,該句在上述代碼中可以找到)。

綜上內(nèi)容,可以知道,對于存在虛函數(shù)的類來說,其對象的typeinfo信息存儲(chǔ)在該類的虛函數(shù)表中。在運(yùn)行時(shí)刻,根據(jù)指針的實(shí)際指向,獲取其typeinfo()信息,從而進(jìn)行相關(guān)操作。

其實(shí),不難看出,上述匯編基本列出了類的對象布局,但仍然不是很清晰,gcc提供了一個(gè)參數(shù)-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)的長度,最后面的代表類名。通過上面內(nèi)存布局信息可以看出,在虛函數(shù)表中存在一項(xiàng)_ZTI7Derived,其中存儲(chǔ)著該對類的類型信息。

如果想要知道其具體名稱,可以使用c++filt來查看,如下:

c++filt_ZTI7Derived
typeinfoforDerived

非多態(tài)

代碼如下:

#include
#include
#include

classMyClss{

};

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

return0;
}

在上述代碼中,實(shí)現(xiàn)了一個(gè)空類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);對應(yīng)匯編的第三行即QWORD PTR [rbp-8], OFFSET FLAT:typeinfo for MyClss,從而可以看出,在編譯期,編譯器已經(jīng)知道了對象的具體信息,進(jìn)而可以在某些情況下,直接由編譯器進(jìn)行替換(比如typeinf().name()操作等)。

dynamic_cast

記得在幾年前的一次面試中,面試官提了個(gè)問題,對于dynamic_cast,如果操作失敗了會(huì)有什么行為?當(dāng)時(shí)對這塊理解的也不深,所以僅僅回答了:對于指針類型轉(zhuǎn)換,如果失敗,則返回NULL,而對于引用,轉(zhuǎn)換失敗就拋出bad_cast。

作為C++開發(fā)人員,基本都知道dynamic_cast是C++中幾個(gè)常用的類型轉(zhuǎn)換符之一,其通過類型信息(typeinfo)進(jìn)行相對安全的類型轉(zhuǎn)換,在轉(zhuǎn)換時(shí),會(huì)檢查轉(zhuǎn)換的src對象是否真的可以轉(zhuǎn)換成dst類型。dynamic_cast轉(zhuǎn)換符只能用于含有虛函數(shù)的類,因此其常常用于運(yùn)行期,對于不包括虛函數(shù)的類,完全可以使用其它幾個(gè)轉(zhuǎn)換符在編譯期進(jìn)行轉(zhuǎn)換。通常來說,其類型轉(zhuǎn)換分為向上轉(zhuǎn)換和向下轉(zhuǎn)換兩種,如下圖所示:


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

實(shí)例代碼如下:

#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向上轉(zhuǎn)換
Derived*d1=dynamic_cast(b1);//downcasting向下轉(zhuǎn)換

return0;
}

實(shí)現(xiàn)

通過查閱資料,發(fā)現(xiàn)dynamic_cast最終會(huì)調(diào)用libstdc++中的__dynamic_cast函數(shù),所以曾經(jīng)以為__dynamic_cast函數(shù)就是dynamic_cast的實(shí)現(xiàn)版本,但是通過對比參數(shù),發(fā)現(xiàn)并非如此:

dynamic_cast(t);//只有一個(gè)參數(shù)

//__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的一個(gè)分支實(shí)現(xiàn)?

為了驗(yàn)證猜測,示例如下:

#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;
}

既然本節(jié)內(nèi)容是dynamic_cast,而只在CheckType()函數(shù)中才有對dynamic_cast的調(diào)用,那么我們著重分析CheckType函數(shù)。

首先,我們通過g++的命令-fdump-class-hierarchy獲取其內(nèi)存布局,Derived內(nèi)存布局如下(需要注意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)

向上轉(zhuǎn)換

在CheckType(Derived*)處,通過gdb進(jìn)行分析,如下:

(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.

為了便于理解,在上述代碼關(guān)鍵部分加上了注釋.

我們注意到,在上述匯編代碼中,沒有找到外部函數(shù)調(diào)用(__dynamic_cast),而僅僅是一些常用的跳轉(zhuǎn)和比較指令。其中,前兩條orl指令的執(zhí)行條件為t不為0,而第三條orl指令的執(zhí)行條件為t不為0且t+16不為0。這幾個(gè)行為是在編譯期完成的,也就是說在本例中,dynamic_cast由編譯器在編譯期實(shí)現(xiàn)了轉(zhuǎn)換,所以可以說其是靜態(tài)轉(zhuǎn)換

在前面的內(nèi)存布局中,Derived對象有3個(gè)偏移量,分別為(Derived/Base1 = 0, Base2 = +0x10),即相對于Derived和Base1其偏移量為0,而相對于Base2其偏移量為16。前兩個(gè)dynamic_cast是Derived* -> Derived* 和 Derived* -> Base1*,都不需要調(diào)整指針,所以在CheckType的if語句中使用t的值作為dynamic_cast的返回值。在第三次Derived* -> Base2*轉(zhuǎn)換中,編譯時(shí)知道地址是t+0x10,所以計(jì)算t+0x10的結(jié)果就是dynamic_cast的返回值。

至此,我們可以說,dynamic_cast操作中,向上轉(zhuǎn)換是靜態(tài)操作,在編譯階段完成

向下轉(zhuǎn)換

在CheckType(Base1*)處,通過gdb進(jìn)行分析,如下:

(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*不進(jìn)行任何轉(zhuǎn)換(這不廢話嘛,類型是相同的)。而對于Base1* -> Derived* 以及 Base1* -> Base2* 則需要調(diào)用__dynamic_cast函數(shù),而其所需要的參數(shù),在匯編指令中也可以看出,下面將對該函數(shù)進(jìn)行詳細(xì)分析。

__dynamic_cast參數(shù)語義

聲明如下:

__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代表需要轉(zhuǎn)換的指針

  • ?src_type原始類型

  • ?dst_type目標(biāo)類型

  • ?src2dst表示從dst到src的偏移量,當(dāng)該值為如下3個(gè)之一時(shí)候,有特殊含義:

    • ?-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 的公共基類,如上節(jié)中的Base1* -> Base2*;-3代表src是多個(gè)(dst的)公共基類并且不是虛基類,即沒有虛擬繼承的菱形繼承。如果不為-1 -2 -3三值之一,則src2dst代表src和dst的偏移,如上一節(jié)中從Base1* -> Base1*轉(zhuǎn)換的時(shí)候傳值為0,即偏移為0;Base1*->Base2*轉(zhuǎn)換的時(shí)候,傳的值為-2(0xfffffffffffffffe)。

__dynamic_cast實(shí)現(xiàn)

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);
...

這個(gè)函數(shù)先通過src_ptr來初始化部分局部變量:

  • ?vtable通過對src_ptr解引用(deref)獲取

  • ?vtable_prefix子對象虛函數(shù)表地址,通過vtable的類型信息和offset_to_top來獲取

  • ?whole_ptrsrc_ptr最底層的派生類地址,一般為src_ptr的值加上offset_to_top

  • ?whole_typesrc_ptr最底層的派生類的虛函數(shù)表中的類型信息(type info)

  • ?whole_vtablewhole對象的虛函數(shù)表地址

然后調(diào)用whole_type->__do_dyncast,而這也是該函數(shù)的核心模塊。然后根據(jù)返回值的內(nèi)容來判斷結(jié)果,并進(jìn)行相應(yīng)的操作。

其中,vtable_prefix的定義如下:

structvtable_prefix
{
//Offsettomostderivedobject.
ptrdiff_twhole_object;
//Pointertomostderivedtype_info.
const__class_type_info*whole_type;
//Whataclass'svptrpointsto.
constvoid*origin;
};
  • ?whole_object 表示當(dāng)前指針指向?qū)ο蟮钠屏?/p>

  • ? whole_type 指向 C++ 對象的類型:class(基類)、si_class(單一繼承類型)、vmi_class(多重或虛擬繼承類型)

  • ? origin 表示虛函數(shù)表的入口,等于實(shí)例的虛指針。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被調(diào)用之后,后面就根據(jù)其出參result的返回值進(jìn)行各種判斷,那么result到底什么意思呢?其實(shí),從上述定義就能看出,whole2dst代表whole對象向dst的轉(zhuǎn)換結(jié)果,而whole2src代表whole對象向src的轉(zhuǎn)換結(jié)果等,通過下面的圖能更加清晰的理解轉(zhuǎn)換過程:

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

在上圖中,有3中類型,src、whole以及dst,__do_dyncast函數(shù)功能則是提供該3中類型的轉(zhuǎn)換結(jié)果,在只有滿足以下3中情況時(shí)候,__dynamic_cast才返回非空:

  • ?src是dst的公共基類

  • ?dst和src不是直接繼承的關(guān)系,但是whole2src和whole2dst都是public

  • ?dst2src未知且whole2src是非public虛繼承關(guān)系,則不使用whole,重新獲取dst和src的關(guān)系

這塊邏輯比較繞,其實(shí)可以將關(guān)系理解為圖上的一條條連接線,節(jié)點(diǎn)理解為類型信息,dynamic_cast的過程,就是判斷有沒有從src到dst有沒有路徑的過程。

繼承關(guān)系

在前面的內(nèi)容中,遇到過vtable for __cxxabiv1::__si_class_type_info+16這種,那么si_class_type_info又是什么呢?同樣,在翻閱了源碼之后,發(fā)現(xiàn)其是gcc中繼承關(guān)系的一種。

在gcc中,將繼承關(guān)系表示為圖結(jié)構(gòu),對于類,有以下三種類型(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 表示沒有繼承關(guān)系的類,__si_class_type_info 表示單繼承的類,__vmi_class_type_info 表示多繼承或虛擬繼承的類。類名開頭的si代表單繼承,vmi代表虛擬或多重繼承。

查看定義,__si_class_type_info 包含指向基類類型的單個(gè)指針,而 __vmi_class_type_info 包含指向基類類型的指針數(shù)組?;愵愋痛鎯?chǔ)其子對象的位置和基類的類型(public、virtual)。

仍然以上一節(jié)中的代碼為例,使用gdb來分析__ZTI7Derived、__ZTI5Base1、__ZTI5Base2的關(guān)系

(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的一個(gè)實(shí)例,其基類數(shù)組的類型分別是_ZTI5Base1和_ZTI5Base2,通過將這些類型展開,就能獲取一張圖結(jié)構(gòu),進(jìn)而說明dynamic_cast的過程就是遍歷圖結(jié)構(gòu)確定路徑關(guān)系的過程,采用的是深度優(yōu)先搜索。

審核編輯 :李倩


聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報(bào)投訴
  • C++
    C++
    +關(guān)注

    關(guān)注

    21

    文章

    2085

    瀏覽量

    73301
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4670

    瀏覽量

    67761
  • 編譯
    +關(guān)注

    關(guān)注

    0

    文章

    646

    瀏覽量

    32664

原文標(biāo)題:C++:從技術(shù)實(shí)現(xiàn)角度聊聊RTTI

文章出處:【微信號:CPP開發(fā)者,微信公眾號:CPP開發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    C++語言基礎(chǔ)知識

    電子發(fā)燒友網(wǎng)站提供《C++語言基礎(chǔ)知識.pdf》資料免費(fèi)下載
    發(fā)表于 07-19 10:58 ?6次下載

    C++實(shí)現(xiàn)類似instanceof的方法

    函數(shù),可實(shí)際上C++中沒有。但是別著急,其實(shí)C++中有兩種簡單的方法可以實(shí)現(xiàn)類似Java中的instanceof的功能。 在 C++ 中,確定對象的類型是編程中實(shí)際需求,使開發(fā)人員
    的頭像 發(fā)表于 07-18 10:16 ?355次閱讀
    <b class='flag-5'>C++</b>中<b class='flag-5'>實(shí)現(xiàn)</b>類似instanceof的方法

    C/C++中兩種宏實(shí)現(xiàn)方式

    #ifndef的方式受C/C++語言標(biāo)準(zhǔn)支持。它不僅可以保證同一個(gè)文件不會(huì)被包含多次,也能保證內(nèi)容完全相同的兩個(gè)文件(或者代碼片段)不會(huì)被不小心同時(shí)包含。
    的頭像 發(fā)表于 04-19 11:50 ?432次閱讀

    鴻蒙OS開發(fā)實(shí)例:【Native C++

    使用DevEco Studio創(chuàng)建一個(gè)Native C++應(yīng)用。應(yīng)用采用Native C++模板,實(shí)現(xiàn)使用NAPI調(diào)用C標(biāo)準(zhǔn)庫的功能。使用C
    的頭像 發(fā)表于 04-14 11:43 ?2163次閱讀
    鴻蒙OS開發(fā)實(shí)例:【Native <b class='flag-5'>C++</b>】

    使用 MISRA C++:2023? 避免基于范圍的 for 循環(huán)中的錯(cuò)誤

    在前兩篇博客中,我們?向您介紹了新的 MISRA C++ 標(biāo)準(zhǔn)?和?C++ 的歷史?。在這篇博客中,我們將仔細(xì)研究以 C++ 中?for?循環(huán)為中心的特定規(guī)則。
    的頭像 發(fā)表于 03-28 13:53 ?515次閱讀
    使用 MISRA <b class='flag-5'>C++</b>:2023? 避免基于范圍的 for 循環(huán)中的錯(cuò)誤

    c語言,c++,java,python區(qū)別

    C語言、C++、Java和Python是四種常見的編程語言,各有優(yōu)點(diǎn)和特點(diǎn)。 C語言: C語言是一種面向過程的編程語言。它具有底層的特性,能夠?qū)τ?jì)算機(jī)硬件進(jìn)行直接操作。
    的頭像 發(fā)表于 02-05 14:11 ?1355次閱讀

    C++在Linux內(nèi)核開發(fā)中爭議到成熟

    Linux 內(nèi)核郵件列表中一篇已有六年歷史的老帖近日再次引發(fā)激烈討論 —— 主題是建議將 Linux 內(nèi)核的開發(fā)語言 C 轉(zhuǎn)換為更現(xiàn)代的 C++。
    的頭像 發(fā)表于 01-31 14:11 ?486次閱讀
    <b class='flag-5'>C++</b>在Linux內(nèi)核開發(fā)中<b class='flag-5'>從</b>爭議到成熟

    C++簡史:C++是如何開始的

    MISRA C++:2023,MISRA? C++ 標(biāo)準(zhǔn)的下一個(gè)版本,來了!為了幫助您做好準(zhǔn)備,我們介紹了 Perforce 首席技術(shù)支持工程師 Frank van den Beuken 博士撰寫
    的頭像 發(fā)表于 01-11 09:00 ?424次閱讀
    <b class='flag-5'>C++</b>簡史:<b class='flag-5'>C++</b>是如何開始的

    C語言和C++中那些不同的地方

    C語言雖說經(jīng)常和C++在一起被大家提起,但可千萬不要以為它們是一個(gè)東西?,F(xiàn)在我們常用的C語言是C89標(biāo)準(zhǔn),C++
    的頭像 發(fā)表于 12-07 14:29 ?772次閱讀
    <b class='flag-5'>C</b>語言和<b class='flag-5'>C++</b>中那些不同的地方

    開箱即用!教你如何正確使用華為云CodeArts IDE for C/C++

    C/C++編碼體驗(yàn)、方便的訪問華為云資源、簡單的引用華為云服務(wù)于一身,實(shí)現(xiàn)C/C++開發(fā)者在個(gè)人研發(fā)作業(yè)體驗(yàn)和效率上的巨大提升。 為了幫助
    的頭像 發(fā)表于 11-29 17:40 ?610次閱讀
    開箱即用!教你如何正確使用華為云CodeArts IDE for <b class='flag-5'>C</b>/<b class='flag-5'>C++</b>!

    c++怎么開始編程

    C++是一種高級的、通用的編程語言,用于開發(fā)各種類型的應(yīng)用程序。它是C語言演變而來,也是一種靜態(tài)類型語言,可以在不同的平臺(tái)上進(jìn)行開發(fā)。C++具有高度的靈活性和性能,并且廣泛應(yīng)用于游戲
    的頭像 發(fā)表于 11-27 15:56 ?731次閱讀

    c++多行注釋快捷鍵

    C++中,多行注釋(也稱為塊注釋)是一種用于注釋大段代碼或多個(gè)語句的方法。當(dāng)你希望暫時(shí)禁用一些代碼或者解釋特定部分代碼的作用時(shí),多行注釋是非常有用的。 在C++中,多行注釋以 /* 開始,以
    的頭像 發(fā)表于 11-22 10:24 ?6761次閱讀

    C++智能指針的底層實(shí)現(xiàn)原理

    C++智能指針的頭文件: #include 1. shared_ptr: 智能指針本質(zhì)上來說是一個(gè)模板類,用類實(shí)現(xiàn)對指針對象的管理。 template class shared_ptr
    的頭像 發(fā)表于 11-09 14:32 ?573次閱讀
    <b class='flag-5'>C++</b>智能指針的底層<b class='flag-5'>實(shí)現(xiàn)</b>原理

    C++之父新作帶你勾勒現(xiàn)代C++地圖

    為了幫助大家解決這些痛點(diǎn)問題,讓大家領(lǐng)略現(xiàn)代C++之美,掌握其中的精髓,更好地使用C++,C++之父Bjarne Stroustrup坐不住了,他親自操刀寫就了這本《C++之旅》!
    的頭像 發(fā)表于 10-30 16:35 ?696次閱讀
    <b class='flag-5'>C++</b>之父新作帶你勾勒現(xiàn)代<b class='flag-5'>C++</b>地圖

    可綜合的RTL代碼的角度聊聊interface

    SystemVerilog引入了interface,這里我們可綜合的RTL代碼的角度聊聊interface。
    的頭像 發(fā)表于 10-12 09:06 ?1497次閱讀
    <b class='flag-5'>從</b>可綜合的RTL代碼的<b class='flag-5'>角度</b><b class='flag-5'>聊聊</b>interface