本系列深入分析編譯器對於C++虛函數的底層實現,最後分析C++在多態的情況下的性能是否有受影響,多態究竟有多大的性能損失。 ...
“深度解讀《深度探索C++對象模型》”系列已經在CSDN上和我的公眾號上更新完畢,請有需要的同學移步到我的CSDN主頁里去閱讀,主頁地址:https://blog.csdn.net/iShare_Carlos?spm=1010.2135.3001.5421
或者敬請關註我的公眾號:iShare愛分享
前面兩篇請從這裡閱讀:
深度解讀《深度探索C++對象模型》之C++虛函數實現分析(一)
深度解讀《深度探索C++對象模型》之C++虛函數實現分析(二)
虛繼承情況下的虛函數和多態的實現分析
虛繼承如果再加上多重繼承關係,或者具有兩層以上的虛繼承關係,那麼編譯器對於虛函數的支持簡直像進了迷宮一樣讓人眼花繚亂,它們的關係讓人撲朔迷離。其實在實際的應用中很少會出現這樣的設計,也不建議這樣做。我們還是以一個較為常用的只有一層的虛繼承關係的例子來講解對於虛函數的支持,如以下的例子:
#include <cstdio>
class Base {
public:
virtual ~Base() = default;
virtual void virtual_func1() { printf("%s\n", __PRETTY_FUNCTION__); }
virtual void virtual_func2() { printf("%s\n", __PRETTY_FUNCTION__); }
int b = 0;
};
class Derived: virtual public Base {
public:
virtual ~Derived() = default;
void virtual_func2() override { printf("%s\n", __PRETTY_FUNCTION__); }
virtual void virtual_func3() { printf("%s\n", __PRETTY_FUNCTION__); }
int d = 0;
};
int main() {
Derived* pd = new Derived;
pd->virtual_func1();
pd->virtual_func2();
pd->virtual_func3();
Base* pb = pd;
pb->virtual_func1();
pb->virtual_func2();
delete pd;
return 0;
}
上面的代碼中繼承關係雖然只是單一繼承,但由於是虛繼承,所以它不像普通的單繼承那樣,基類的子類部分和對象的起始地址是對齊的,虛函數表也共用同一個,由於虛繼承的關係,虛基類的子類部分是共用的,一般編譯器的實現會把它放到對象佈局的最尾端,即在所有具體繼承的子對象和子類之後,也不和任何子對象共用虛函數表,它自己單獨擁有一個虛函數表。所以上面的代碼編譯器將會產生兩個虛函數表,一個是Derived子類的,一個是Base虛基類的,只不過編譯器把兩個表合併在一起,兩個子對象(Derived和Base)的虛函數表指針被設置指向不同的偏移地址,看看上面代碼對應的彙編代碼中的虛函數表:
vtable for Derived:
.quad 16
.quad 0
.quad typeinfo for Derived
.quad Derived::~Derived() [complete object destructor]
.quad Derived::~Derived() [deleting destructor]
.quad Derived::virtual_func2()
.quad Derived::virtual_func3()
.quad -16
.quad 0
.quad -16
.quad -16
.quad typeinfo for Derived
.quad virtual thunk to Derived::~Derived() [complete object destructor]
.quad virtual thunk to Derived::~Derived() [deleting destructor]
.quad Base::virtual_func1()
.quad virtual thunk to Derived::virtual_func2()
Derived對象的虛函數表被設置指向上面的第5行的位置,Base虛基類的虛函數表被設置指向第14行的位置,這些事情都是編譯器在預設析構函數中生成的代碼來完成的,具體的分析可以見另外一篇文章《深度解讀<深度探索C++對象模型>之預設構造函數》。因為虛繼承的存在,上面的表中除了支持多態的虛函數和RTTI信息外,還包含了支持虛繼承的信息,主要就是一些正負偏移值,用來在有需要時調整this指針,如第2行的16就是從Derived對象的起始地址調整到Base虛基類子對象的起始地址,第9到12行的-16用於從Base虛基類子對象調整回Derived對象的起始地址。上面部分是主表,下麵部分是次表,主表中是Derived類定義的虛函數:虛析構函數、virtual_func2和virtual_func3兩個虛函數,次表是從Base虛基類繼承而來的虛函數,包括了虛析構函數、virtual_func1和virtual_func2兩個虛函數,其中虛析構函數和virtual_func2虛函數在Derived類中進行了改寫,所以這裡存放的不是真正的虛函數實例的地址,而是指向thunk技術實現的一段彙編代碼,彙編代碼里會跳轉到真實的虛函數實例中執行。
虛繼承下支持虛函數的困難點主要在於兩方面:一個是通過Derived類型的指針調用Base虛基類中的虛函數;另一個是通過Base虛基類類型的指針調用Derived類的虛函數。它們的調用關係跟多重繼承下處理第二及後繼基類的方式很相似,下麵我們以這兩點分別來講解。
- 通過Derived類型的指針調用Base虛基類中的虛函數
在上面C++代碼中的第20到22行的三行調用中,對virtual_func2和virtual_func3虛函數的調用,因為這兩個虛函數存在於Derived類的虛函數表中,所以對這兩個的調用採用的是常規的調用方法。對virtual_func1虛函數的調用,因為virtual_func1虛函數是從Base虛基類繼承來的且在Derived類中沒有進行改寫,因此它只存在於Base虛基類的虛函數表中,調用它之前先要進行this指針的調整,讓this指針指向Base子對象的起始地址,再通過Base子對象的虛函數表指針來定址到它的虛函數表,並調用對應的虛函數,下麵是它的彙編代碼:
mov rax, qword ptr [rbp - 16]
mov rcx, qword ptr [rax]
mov rcx, qword ptr [rcx - 24]
mov rdi, rax
add rdi, rcx
mov rax, qword ptr [rax + rcx]
call qword ptr [rax + 16]
[rbp - 16]棧空間存放的是Derived對象的起始地址,對其取值即是虛函數表指針(如不熟悉請參考《深度解讀<深度探索C++對象模型>之C++對象的記憶體佈局》),它指向的是Derived類的虛函數表的起始地址,也即是上表中的第5的位置,[rcx - 24]的意思是往上偏移24位元組並取值,往上偏移24位元組即指向了表的開頭位置,它的值是16,這個值就是上面介紹的用於支持虛繼承調整this指針的作用,然後上面彙編代碼的第4、5行把它加到rdi上,rdi寄存器存放的是Derived對象的起始地址,rdi寄存器(作為this指針)也將作為第7行調用虛函數時的參數。第6行的[rax + rcx]的意思是Derived對象的起始地址加上16偏移值然後取值,它是Base子對象的虛函數表指針(指向上表中的第14行),然後在第7行代碼的調用時再加上16的偏移值即是virtual_func1虛函數對應的地址,即上表中的第16行。
- 通過Base虛基類類型的指針調用Derived類的虛函數
通過Base虛基類類型的指針調用Derived類的虛析構函數和virtual_func2虛函數,採用的是相同的實現方法,即thunk技術。所以放在一起來講,先來看下它們的彙編代碼:
virtual thunk to Derived::~Derived() [deleting destructor]: # @virtual thunk to Derived::~Derived() [deleting destructor]
push rbp
mov rbp, rsp
mov qword ptr [rbp - 8], rdi
mov rdi, qword ptr [rbp - 8]
mov rax, qword ptr [rdi]
mov rax, qword ptr [rax - 24]
add rdi, rax
pop rbp
jmp Derived::~Derived() [deleting destructor] # TAILCALL
# 另一個虛析構函數的代碼差不多,這裡省略
virtual thunk to Derived::virtual_func2(): # @virtual thunk to Derived::virtual_func2()
push rbp
mov rbp, rsp
mov qword ptr [rbp - 8], rdi
mov rdi, qword ptr [rbp - 8]
mov rax, qword ptr [rdi]
mov rax, qword ptr [rax - 40]
add rdi, rax
pop rbp
jmp Derived::virtual_func2() # TAILCALL
通過Base類型的指針來調用Derived類的虛析構函數的場景是:Base類型的指針指向Derived的對象,然後調用了delete函數釋放這個對象,這時調用的是在Base子對象的虛函數表中的虛析構函數,它是thunk技術實現的一段彙編代碼。virtual_func2虛函數定義在Derived類中,又是對Base虛基類中的virtual_func2虛函數的改寫,所以存在於兩個虛函數表中,但實際的函數實例只有一個,在Base虛基類的虛函數表中存放的是thunk技術實現的一段彙編代碼。
上面的兩個函數都是thunk技術生成的彙編代碼,代碼的內容基本一樣,只是在最後一行跳轉到不同的函數中去執行。首先將this指針(保存在rdi寄存器中,這時指向Base子對象的地址)保存到[rbp - 8]的棧空間中,然後取值並保存到rax寄存器中,這裡取到的值是Base子對象中的虛函數表指針,即指向上表中第14行的位置,然後減去24(或40)的偏移量並取值,這兩處的值都是-16,然後加上rdi中,rdi保存的是Base子對象的地址,向下偏移16位元組後回到Derived對象的起始地址,然後跳轉到相應的函數中去執行。
“深度解讀《深度探索C++對象模型》”系列已經在CSDN上和我的公眾號上更新完畢,請有需要的同學移步到我的CSDN主頁里去閱讀,主頁地址:https://blog.csdn.net/iShare_Carlos?spm=1010.2135.3001.5421
或者敬請關註我的公眾號:iShare愛分享