這篇文章是C++ RTTI的后續,前面我們介紹了typeid()操作符,這篇文章介紹RTTI的另一個概念,即dynamic_cast。
相比較typeid,dynamic_cast在實際項目中會被大量使用。
首先,一句話dynamic_cast是干什么用的,dynamic_cast是在兩個類之間做類型轉換的,即把一個指針類型轉換成另一個類型;這個轉換過程是在運行時刻轉換的,所以叫dynamic_cast(與此相對的是static_cast)。dynamic_cast的使用必須滿足下面兩個條件:
- 如果兩個類之間沒有父子關系,那么source必須是多態的。
- 如果是從父類到子類的轉換,那么父類(source)也必須多態的。
否則編譯器就報錯:
<file>:41: error: cannot dynamic_cast 'pc' (of type 'class <SRC> *') to type 'class <DST> *' (source type is not polymorphic)
我們重新解釋一下上述描述:dynamic_cast的使用需要source是多態的,只有一種情況例外,那就是從子類到父類的轉換。再換一個角度,其實dynamic_cast的使用必須要source是多態的,因為同學們想想,例外情況中子類到父類的轉換是天然的行為,根本不需要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,根本沒有調用到dynamic_cast,因為編譯器明確知道父類和子類的關系以及成員構成,又沒有多態的問題,所以編譯器就能夠完成類型轉換工作,其實此時dynamic_cast是被映射成了static_cast使用。
討論完使用場合,下面我們看看轉換的結果(假定source都是多態的):
- 如果source和dest沒有父子關系,那么結果是NULL,即使兩個類定義成一模一樣。
- 從子類到父類,前面說過必然是成功的。
- 從父類到子類,這也是dynamic_cast最常用的情況,這依賴于父類指針是否真實的指向了對應的子類類型,標示這是不是一個真實的子類對象。
#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;
}
運行結果如下:
NOT NULL
NULL
可見b1是能轉換成功的,b2不能成功,因為雖然b1和b2都是A的實例,但是b2不是B1的實例。
前面我們介紹了dynamic_cast的使用場景,現在我們介紹dynamic_cast是如何工作的;代碼說明問題:
#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()函數代碼:
_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
首先驗證參數pa是否為NULL,如果是則直接返回NULL,如果不是則調用C++ lib庫函數__dynamic_cast。
庫函數__dynamic_cast需要四個參數:
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)
參數聲明為:
- v: source對象地址,NOT NULL(前面源代碼我們看的如果是NULL就不會進到這兒來了),并且由于source是多態的,那么source對象的第一個域是指向虛函數表的指針。
- src: source對象的類類型
- dat: destination對象的類類型
- 這個參數我也沒弄清楚,但是當src是dst的基類時為-2,當src和dst不相干時為0。
關于這個函數的詳細說明搜搜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.
*/
下面我們再看src(class A)和dst(class B)兩個參數的值定義:
_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
參數src的類型時$_ZTI1A, 參數dst的類型時$_ZTI1B;從內容可以看出這兩個類型的定義也是不一樣的
- $_ZTI1A的類型是__class_type_info
- $_ZTI1B的類型是__si_class_type_info,是__class_type_info的子類;子類包含一個指向父類類類型的指針:
const __class_type_info* __base_type;
參閱 /usr/include/c++/4.4.4/cxxabi.h
所有這些類的類型信息都在應用程序啟動的時候,在main函數入口之前注冊到全局變量里,使得在用戶程序能夠訪問到他們。
最后總結一下dynamic_cast的使用場景
- dynamic_cast用來實現指針類型的轉化。
- dynamic_cast如果轉化成功則返回指向目標類型的指針,如果轉換不成功返回NULL。
- dynamic_cast要求src必須是多態的,因為dynamic_cast需要從類的虛函數表表中獲得類類型信息。
- dynamic_cast最常見的用法是從一個抽象基類轉換到具體的實現類。
當一個父類有多種子類時,如果目前有一個指向父類的指針,但是我們不知道指向父類的指針實際上指向的是哪一種子類,可以使用dynamic_cast<Child *>來判斷,如果返回不是NULL,說明這是一個指向Child子類的指針,否則就不是。