這篇文章是C++ RTTI的后續(xù),前面我們介紹了typeid()操作符,這篇文章介紹RTTI的另一個(gè)概念,即dynamic_cast。
相比較typeid,dynamic_cast在實(shí)際項(xiàng)目中會(huì)被大量使用。
首先,一句話dynamic_cast是干什么用的,dynamic_cast是在兩個(gè)類之間做類型轉(zhuǎn)換的,即把一個(gè)指針類型轉(zhuǎn)換成另一個(gè)類型;這個(gè)轉(zhuǎn)換過(guò)程是在運(yùn)行時(shí)刻轉(zhuǎn)換的,所以叫dynamic_cast(與此相對(duì)的是static_cast)。dynamic_cast的使用必須滿足下面兩個(gè)條件:
- 如果兩個(gè)類之間沒(méi)有父子關(guān)系,那么source必須是多態(tài)的。
- 如果是從父類到子類的轉(zhuǎn)換,那么父類(source)也必須多態(tài)的。
否則編譯器就報(bào)錯(cuò):
<file>:41: error: cannot dynamic_cast 'pc' (of type 'class <SRC> *') to type 'class <DST> *' (source type is not polymorphic)
我們重新解釋一下上述描述:dynamic_cast的使用需要source是多態(tài)的,只有一種情況例外,那就是從子類到父類的轉(zhuǎn)換。再換一個(gè)角度,其實(shí)dynamic_cast的使用必須要source是多態(tài)的,因?yàn)橥瑢W(xué)們想想,例外情況中子類到父類的轉(zhuǎn)換是天然的行為,根本不需要dynamic_cast啊,像(Parent *)child。
我們看一段代碼:
#include <stdio.h>
#include <string>
class A1 {};
class A2 : public A1 {};
void foo(A2 * pa2) {
A1 * va1 = dynamic_cast<A1 *>(pa2);
}
int main(int argc, char * argv[]) {
A2 * a2 = new A2();
foo(a2);
return 0;
}
編譯器生成的匯編代碼如下:
_Z3fooP2A2:
pushq %rbp
movq %rsp, %rbp
movq %rdi, -24(%rbp)
movq -24(%rbp), %rax
movq %rax, -8(%rbp)
leave
ret
可見編譯器直接把pa2的賦給了va1,根本沒(méi)有調(diào)用到dynamic_cast,因?yàn)榫幾g器明確知道父類和子類的關(guān)系以及成員構(gòu)成,又沒(méi)有多態(tài)的問(wèn)題,所以編譯器就能夠完成類型轉(zhuǎn)換工作,其實(shí)此時(shí)dynamic_cast是被映射成了static_cast使用。
討論完使用場(chǎng)合,下面我們看看轉(zhuǎn)換的結(jié)果(假定source都是多態(tài)的):
- 如果source和dest沒(méi)有父子關(guān)系,那么結(jié)果是NULL,即使兩個(gè)類定義成一模一樣。
- 從子類到父類,前面說(shuō)過(guò)必然是成功的。
- 從父類到子類,這也是dynamic_cast最常用的情況,這依賴于父類指針是否真實(shí)的指向了對(duì)應(yīng)的子類類型,標(biāo)示這是不是一個(gè)真實(shí)的子類對(duì)象。
#include <stdio.h>
#include <string>
class A {
public:
virtual ~A() {}
};
class B1: public A {};
class B2: public A {};
void foo(A * pa) {
B1 * vb1 = dynamic_cast<B1 *>(pa);
printf("%s\n", vb1 == NULL ? "NULL" : "NOT NULL");
}
int main(int argc, char * argv[]) {
B1 * b1 = new B1();
B2 * b2 = new B2();
foo(b1);
foo(b2);
return 0;
}
運(yùn)行結(jié)果如下:
NOT NULL
NULL
可見b1是能轉(zhuǎn)換成功的,b2不能成功,因?yàn)殡m然b1和b2都是A的實(shí)例,但是b2不是B1的實(shí)例。
前面我們介紹了dynamic_cast的使用場(chǎng)景,現(xiàn)在我們介紹dynamic_cast是如何工作的;代碼說(shuō)明問(wèn)題:
#include <stdio.h>
#include <string>
class A {
public:
virtual ~A() {}
};
class B: public A {};
void foo(A * pa) {
B * vb = dynamic_cast<B *>(pa);
}
int main(int argc, char * argv[]) {
B * b = new B();
foo(b);
return 0;
}
類A是類B的父類,我們看生成的foo()函數(shù)代碼:
_Z3fooP1A:
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
movq %rdi, -24(%rbp)
movq -24(%rbp), %rax
testq %rax, %rax
jne .L9
movl $0, %eax
jmp .L10
.L9:
movl $0, %ecx
movl $_ZTI1B, %edx
movl $_ZTI1A, %esi
movq %rax, %rdi
call __dynamic_cast
.L10:
movq %rax, -8(%rbp)
leave
ret
首先驗(yàn)證參數(shù)pa是否為NULL,如果是則直接返回NULL,如果不是則調(diào)用C++ lib庫(kù)函數(shù)__dynamic_cast。
庫(kù)函數(shù)__dynamic_cast需要四個(gè)參數(shù):
extern "C" void*
__dynamic_cast (const void *v,
const abi::__class_type_info *src,
const abi::__class_type_info *dst,
std::ptrdiff_t src2dst_offset)
參數(shù)聲明為:
- v: source對(duì)象地址,NOT NULL(前面源代碼我們看的如果是NULL就不會(huì)進(jìn)到這兒來(lái)了),并且由于source是多態(tài)的,那么source對(duì)象的第一個(gè)域是指向虛函數(shù)表的指針。
- src: source對(duì)象的類類型
- dat: destination對(duì)象的類類型
- 這個(gè)參數(shù)我也沒(méi)弄清楚,但是當(dāng)src是dst的基類時(shí)為-2,當(dāng)src和dst不相干時(shí)為0。
關(guān)于這個(gè)函數(shù)的詳細(xì)說(shuō)明搜搜C++ ABI文檔,比如: https://android.googlesource.com/platform/abi/cpp/+/6426040f1be4a844082c9769171ce7f5341a5528/src/dynamic_cast.cc
#define DYNAMIC_CAST_NO_HINT -1
#define DYNAMIC_CAST_NOT_PUBLIC_BASE -2
#define DYNAMIC_CAST_MULTIPLE_PUBLIC_NONVIRTUAL_BASE -3
/* v: source address to be adjusted; nonnull, and since the
* source object is polymorphic, *(void**)v is a virtual pointer.
* src: static type of the source object.
* dst: destination type (the "T" in "dynamic_cast<T>(v)").
* src2dst_offset: a static hint about the location of the
* source subobject with respect to the complete object;
* special negative values are:
* -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
* otherwise, the src type is a unique public nonvirtual
* base type of dst at offset src2dst_offset from the
* origin of dst.
*/
下面我們?cè)倏磗rc(class A)和dst(class B)兩個(gè)參數(shù)的值定義:
_ZTS1B:
.string "1B"
_ZTS1A:
.string "1A"
_ZTI1B:
.quad _ZTVN10__cxxabiv120__si_class_type_infoE+16
.quad _ZTS1B
.quad _ZTI1A
_ZTI1A:
.quad _ZTVN10__cxxabiv117__class_type_infoE+16
.quad _ZTS1A
參數(shù)src的類型時(shí)$_ZTI1A, 參數(shù)dst的類型時(shí)$_ZTI1B;從內(nèi)容可以看出這兩個(gè)類型的定義也是不一樣的
- $_ZTI1A的類型是__class_type_info
- $_ZTI1B的類型是__si_class_type_info,是__class_type_info的子類;子類包含一個(gè)指向父類類類型的指針:
const __class_type_info* __base_type;
參閱 /usr/include/c++/4.4.4/cxxabi.h
所有這些類的類型信息都在應(yīng)用程序啟動(dòng)的時(shí)候,在main函數(shù)入口之前注冊(cè)到全局變量里,使得在用戶程序能夠訪問(wèn)到他們。
最后總結(jié)一下dynamic_cast的使用場(chǎng)景
- dynamic_cast用來(lái)實(shí)現(xiàn)指針類型的轉(zhuǎn)化。
- dynamic_cast如果轉(zhuǎn)化成功則返回指向目標(biāo)類型的指針,如果轉(zhuǎn)換不成功返回NULL。
- dynamic_cast要求src必須是多態(tài)的,因?yàn)閐ynamic_cast需要從類的虛函數(shù)表表中獲得類類型信息。
- dynamic_cast最常見的用法是從一個(gè)抽象基類轉(zhuǎn)換到具體的實(shí)現(xiàn)類。
當(dāng)一個(gè)父類有多種子類時(shí),如果目前有一個(gè)指向父類的指針,但是我們不知道指向父類的指針實(shí)際上指向的是哪一種子類,可以使用dynamic_cast<Child *>來(lái)判斷,如果返回不是NULL,說(shuō)明這是一個(gè)指向Child子類的指針,否則就不是。