摘要訊息 : 編碼器是如何為 C++ 多型類別進行記憶體佈局的?

0. 前言

我們在文章《【C++】記憶體對位》中介紹了不帶有虛擬函式的類別在記憶體中是如何佈局的, 一般所有成員變數都需要對齊到一定的位置. 那麼在 C++ 中, 那些帶有虛擬函式或者通過虛擬繼承實作的衍生類別又是如何在記憶體中佈局的呢?

由於 C++ 標準沒有規定 C++ 多型類別和動態繫結的實作方式, 所以本文僅能給出大致的分析, 不同編碼器下的表現可能不太相同.

1. 虛擬函式表

雖然 C++ 標準沒有規定編碼器應該通過什麼樣的方式來實作 C++ 的多型和動態繫結, 但是幾乎所有編碼器在這個問題上都採用了虛擬函式表 (virtual table, vtbl) 這種方式. 需要注意的是, 普通類別 (沒有虛擬函式也不帶有虛擬繼承) 是不存在虛擬函式表的, 虛擬函式表是多型類別獨有的. 表中除了儲存著一些包含類別的型別信息之外, 主要用於儲存虛擬函式真實指向的記憶體位址. 假設有兩個類別 :

struct A {
    int a;
    virtual void f1();
};
struct B : A {
    int b;
    virtual void f1() override;
    virtual void f2();
};

編碼器在處理完 AB 之後, 會為這兩個類別加上一個額外的指標, 我們稱為虛擬函式表指標 (virtual table pointer, vptr). 如果類別中存在非靜態成員變數, 那麼虛擬函式表指標應該放在類別的最前面, 中間某個位置還是類別的最後呢? 我們只需要做出一個假設 : 假如編碼器將虛擬函式表指標放在了類別非頭部的位置, 那麼要找到不同類別的虛擬函式表指標, 其偏移量就不一樣. 這樣就需要額外為表中增加一個變數, 這個變數用於標記類別開始位置到虛擬函式表指標的偏移量是多少. 而如果直接將虛擬函式表指標放在每一個多型類別的頭部, 就不需要額外增加一個變數去標記了. 所以幾乎所有編碼器都將虛擬函式表指標放在了多型類別開始的位置. 那麼類別 AB 最終類似於這樣 :

struct A {
    void *__virtual_table;
    int a;
    virtual void f1();
};
struct B : A {
    void *__virtual_table;
    int b;
    virtual void f1() override;
    virtual void f2();
};

然而此時又產生了一個問題 : 類別 AB 中的虛擬函式表重複了, 重複的虛擬函式表會導致 sizeof(B) 額外多出一個指標的大小. 因此, 編碼器一般都會將兩個類別的虛擬函式表進行合併, 這就相當於從 A 那裡繼承了表, 然後向裡面添加關於 B 的信息 :

struct A {
    void *__virtual_table;
    int a;
    virtual void f1();
};
struct B : A {
    // inherited __virtual_table from A then merge virtual table
    int b;
    virtual void f1() override;
    virtual void f2();
};

如果 B 從多個基礎類別衍生, 編碼器並不會合併多個虛擬函式表, B 中虛擬函式表的數量為 \displaystyle {\text {衍生列表中基礎類別的數量}}. 例如類別 C 同時繼承了 AB, 那麼編碼器會將 C 的虛擬函式表和衍生列表第一個類別的虛擬函式表合併, 然後從剩餘的衍生列表中繼承它們的虛擬函式表 :

struct A {
    void *__virtual_table;
    virtual void f1();
};
struct B {
    void *__virtual_table;
    virtual void f2();
};
struct C : A, B {
    void *__virtual_table_for_A_and_C;        // inherited from A::__virtual_table and merge the information from C to it
    void *__virtual_table_for_B;        // inherited from B::__virtual_table
};

1.1 簡單的虛擬函式表結構

即然叫虛擬函式表, 那麼表中肯定儲存著虛擬函式真實指向的記憶體位址. 我們可以想像虛擬函式表的結構大致是這樣的 :

#include <typeinfo>

class std::type_info {
private:
    const char *__type_name;
    long __implement_order;
public:
    // ...
};

struct __virtual_table {
    std::type_info __RTTI;
    void *__virtual_function_pointer_1;
    void *__virtual_function_pointer_2;
    // ...
};

其中, __virtual_table 的第一個成員變數 __RTTI 的型別為 std::type_info 用於支援 typeid 關鍵字, 其成員 __type_name 用於支援 typeid(T).name(), __implement_order 用於支援 typeid(T).before(); 接下來就是類別虛擬成員函式的函式指標, 指向了真正要呼叫的函式記憶體位址. 對於 Code 1-3 中的類別 B 來說, 其虛擬函式表類似於

#include <typeinfo>

struct A {
    int a;
    virtual void f1();
};
struct B : A {
    int b;
    virtual void f1() override;
    virtual void f2();
};

struct __virtual_table_for_B {
    std::type_info __RTTI;
    void *__vptr_f1;
    void *__vptr_f2;
};

1.2 多型類別的建構子與解構子

編碼器在向多型類別插入虛擬函式表指標之後, 必定還要在類別的建構子和解構子中插入一些額外的操作, 以保證虛擬函式表可以幫助多型類別正確運作. 以 Code 1-3 中的類別 A 為例, 雖然我們沒有為類別 A 手動撰寫建構子和解構子, 但是編碼器確會在其建構子和解構子中插入額外的程式碼 :

#include <typeinfo>

struct __virtual_table_for_A {
    std::type_info __RTTI;
    void *__vptr_f1;
};
struct __virtual_table_for_B {
    std::type_info __RTTI;
    void *__vptr_f1;
    void *__vptr_f2;
};

template <typename Class, typename Function>
union __cast_to_void_ptr {
    Function Class::*memfn_ptr;
    void *to_void_ptr;
};

struct B;
void (B::*__B_f1_ptr)();
void (B::*__B_f2_ptr)();

struct A {
private:
    enum __from {
        __from_A, __from_B
    };
private:
    void *__virtual_table;
public:
    int a;
private:
    A(__from __type) {
        if(__type == __from_A) {
            this->__virtual_table = operator new(sizeof(__virtual_table_for_A));
            // set __virtual_table_for_A::__RTTI
            // ...
            __cast_to_void_ptr<A, void ()> cast {};
            cast.memfn_ptr = &A::f1;
            static_cast<__virtual_table_for_A *>(this->__virtual_table)->__vptr_f1 = cast.to_void_ptr;
            // ...
        }else {
            this->__virtual_table = operator new(sizeof(__virtual_table_for_B));
            // set __virtual_table_for_A::__RTTI
            // ...
            __cast_to_void_ptr<B, void ()> cast {};
            cast.memfn_ptr = __B_f1_ptr;
            static_cast<__virtual_table_for_B *>(this->__virtual_table)->__vptr_f1 = cast.to_void_ptr;
            cast.memfn_ptr = __B_f2_ptr;
            static_cast<__virtual_table_for_B *>(this->__virtual_table)->__vptr_f2 = cast.to_void_ptr;
            // ...
        }
    }
public:
    A() : A(__real_type) {}
public:
    virtual void f1();
};

Code 4 中, __cast_to_void_ptr 的作用是將成員函式指標轉換為 void *, 因為 C++ 並不允許直接將成員函式指標透過 static_cast, reinterpret_cast 或者 C-Style 轉型轉換為 void *, 因此我們借助等位來間接完成轉型. 類別 A 中被添加了一個列舉, 用於標記類似於 A *tmp {new T {}} 中的 TA 還是 B. 如果真實型別是 A, 那麼配置的記憶體大小為 sizeof(__virtual_table_for_A), 否則配置的記憶體大小為 sizeof(__virtual_table_for_B). 接著就是設定 __RTTI 和對應的成員函式指標真實指向的記憶體位址. 接下來不管向虛擬函式表中添加什麼樣的實體, 我們只需要在建構子中處理這個實體即可.

1.3 編碼期可確定的虛擬函式定位

Code 5 的模擬中, 我們已經可以看到編碼器對虛擬函式的排列是按照虛擬函式的宣告順序進行的. 如果有多個基礎類別, 那麼虛擬函式表中的虛擬函式就按照衍生列表順序從前至後, 然後按照基礎類別中虛擬函式出現的順序來排列. 在基礎類別的虛擬函式完成排列之後, 最後加入衍生類別中的虛擬函式. 例如, 若類別 A 中有虛擬函式 f1, 類別 B 中有虛擬函式 f2, 類別 C 從類別 AB 繼承, 並擁有自己的虛擬函式 f3, 那麼類別 C 的虛擬函式表應該是這樣的 :

#include <typeinfo>

struct __virtual_table_for_C {
    std::type_info __RTTI;
    void *__vptr_f1;        // from A
    void *__vptr_f2;        // from B
    void *__vptr_f3;        // from C
};

根據這種順序, 編碼器在編碼期就可以確定虛擬函式的偏移量. 例如對於 tmp->f2() 這樣的成員函式呼叫, 編碼器只要找到 __vptr_f1, 然後向後偏移一個指標的大小便可以找到成員函式 f2.

1.4 虛擬函式指標指向什麼?

重新回到 Code 5. 對於類別 A 的使用過程中, 是 typeid(A) 的使用次數更多還是其成員函式 f1 的使用次數更多呢? 很顯然, 在一般情況下成員函式的使用次數遠遠多於 typeid 運算子的使用次數. 按照 Code 5 的寫法, 如果讓 __virtual_table 直接指向 __virtual_table_for_A::__RTTI, 那麼每次成員函式的呼叫都需要向後偏移至少一個指標大小. 如果我們從來都沒有使用過 typeid 運算子, 那麼程式的效能就會因為虛擬函式的呼叫損失一些. 因此幾乎所有編碼器都會讓 __virtual_table 直接指向第一個虛擬成員函式, 而不是運作時型別識別的信息. 也就是在類別 A 下, __virtual_table 指向的應該是 __virtual_table_for_A::__vptr_f1. 我們再將 Code 5 補充完整一些 :

#include <typeinfo>

struct __virtual_table_for_A {
    std::type_info __RTTI;
    void *__vptr_f1;
};
struct __virtual_table_for_B {
    std::type_info __RTTI;
    void *__vptr_f1;
    void *__vptr_f2;
};

template <typename Class, typename Function>
union __cast_to_void_ptr {
    Function Class::*memfn_ptr;
    void *to_void_ptr;
};

struct B;
void (B::*__B_f1_ptr)();
void (B::*__B_f2_ptr)();

struct A {
private:
    enum __from {
        __from_A, __from_B
    };
private:
    void *__virtual_table;
public:
    int a;
private:
    A(__from __type) {
        if(__type == __from_A) {
            this->__virtual_table = operator new(sizeof(__virtual_table_for_A));
            // set __virtual_table_for_A::__RTTI
            // ...
            __cast_to_void_ptr<A, void ()> cast {};
            cast.memfn_ptr = &A::f1;
            static_cast<__virtual_table_for_A *>(this->__virtual_table)->__vptr_f1 = cast.to_void_ptr;
            this->__virtual_table = static_cast<char *>(this->__virtual_table) + sizeof(char *);
            // ...
        }else {
            this->__virtual_table = operator new(sizeof(__virtual_table_for_B));
            // set __virtual_table_for_A::__RTTI
            // ...
            __cast_to_void_ptr<B, void ()> cast {};
            cast.memfn_ptr = __B_f1_ptr;
            static_cast<__virtual_table_for_B *>(this->__virtual_table)->__vptr_f1 = cast.to_void_ptr;
            cast.memfn_ptr = __B_f2_ptr;
            static_cast<__virtual_table_for_B *>(this->__virtual_table)->__vptr_f2 = cast.to_void_ptr;
            this->__virtual_table = static_cast<char *>(this->__virtual_table) + sizeof(char *);
            // ...
        }
    }
public:
    virtual void f1();
};

如果我們使用了 typeid 運算子, 處理方式也很簡單 : 只需要讓 __virtual_table 向前偏移一個指標的大小即可. 當然, 如果有些類別全程沒有使用過 typeiddynamic_cast 運算子, 編碼器是可以優化掉 __RTTI 的.

2. this 指標

在任何一個類別中, this 指標必定指向類別開始的位置. 其中, 這也包括了空類別, 因為空類別在記憶體中的佔位至少為一位元組, 因此空類別的 this 指標就會指向那一位元組的位置. 也就是在多型類別中, this 指標實際指向了虛擬函式表指標. 我們是否可以通過 this 指標直接呼叫某個成員函式呢? 根據 Code 7 的實作, 我們可以編寫以下程式碼 :

#include <iostream>

struct A {
    int a;
    virtual void f1() {
        std::cout << 1;
    }
};

template <typename C, typename F>
union to_memfn_ptr {
    F C::*memfn_ptr;
    void *void_ptr;
};

int main(int argc, char *argv[]) {
    A a;
    auto this_adderss {reinterpret_cast<unsigned long>(&a)};        // get this address
    auto vtable_address {this_adderss};     // virtual table address equal to this address
    auto f1_address {*reinterpret_cast<unsigned long *>(vtable_address)};       // get virtual member function f1 address
    to_memfn_ptr<A, void ()> c {};
    c.void_ptr = reinterpret_cast<unsigned long *>(f1_address);
    auto real_type_f1_address {c.memfn_ptr};        // // convert pointer to the type void (A::*)()
    (a.*real_type_f1_address)();        // 輸出結果 : 1
}

有些人可能受到 Code 8 的啟發 : 我們是否可以通過修改 *reinterpret_cast<unsigned long *>(vtable_address) 來入侵類別 A, 將虛擬函式 f1 的真實地址指向另一個函式呢? 理論上完全可行, 但是實際上一般不行. 編碼器在編碼的時候並不是簡單地將我們的程式碼翻譯為機器指令, 期間也會對函式的地址等加上防護, 避免函式地址被指向一個惡意的外部函式, 從而導致一些安全問題. 這種安全防護預設情況下是開啟的, 需要人為手動關閉.

2.1 多繼承下的虛擬函式表

在多重繼承下, 每個繼承層次都有且唯有一個基礎類別, 最高層的衍生類別的虛擬函式表是對基礎類別的虛擬函式表不斷合併而來的, 因此最高層的虛擬函式表也只有一個. 在多重繼承中, this 指標總是指向衍生類別中唯一的虛擬函式表. 但是在多繼承下, 情況就不太相同了. 由於衍生列表中存在多個基礎類別, 最高層的衍生類別在合併虛擬函式表的時候只能選取衍生列表中第一個基礎類別對應的虛擬函式表, 所以最高層的衍生類別需要繼承衍生列表中剩餘基礎類別的虛擬函式表. 這就導致 this 指標不一定總是指向某一個虛擬函式表. 以下面程式碼為例, 設有類別 A, BC 滿足 :

struct A {
    int a;
    virtual void f1();
};
struct B {
    int b;
    virtual void f2();
};
struct C : A, B {
    int c;
    virtual void f1() override;
    virtual void f2() override;
    virtual void f3();
};

類別 C 的實際佈局應該是這樣的 :

#include <typeinfo>

struct __virtual_table_for_B;

struct __virtual_table_for_C {
    std::type_info __RTTI_for_A_and_C;
    void *__vptr_f1;
    void *__vptr_f2;
    void *__vptr_f3;
};

現在若宣告 B *b {new C {}};, 那麼 bthis 指標應該指向哪裡呢? 如果直接讓 this 指標指向 __vptr_f1, 那麼 b 的型別就會變成 A 或者 C, 況且 B 中根本就沒有一個名稱為 f1 的虛擬成員函式. 這樣來看, 比較合適的結果是讓 this 指向 __vptr_f2. 但是如果我們對 b 使用了 typeid 運算子, this 向後偏移一個指標大小後得到的 __vptr_f1 根本無法支援 typeid 運算. 我們可能會想到在 __vptr_f2 前面增加一個 std::type_info __RTTI_for_B;, 這樣做又會回到最開始的問題 : 每個虛擬函式的偏移量不再固定, 編碼器需要加入額外的標識變數以幫助虛擬成員函式指標找到正確的指向. 比較好的解決方案似乎是直接將 __virtual_table_for_B 繼承過來, 放到類別最後 :

#include <typeinfo>

struct __virtual_table_for_B;

struct __virtual_table_for_C {
    std::type_info __RTTI_for_A_and_C;
    void *__vptr_f1;
    void *__vptr_f2;
    void *__vptr_f3;
    __virtual_table_for_B *__vtbl_B;
};

但是當我們需要類別 B 虛擬函式表中內容的時候, 就需要再次對 __vtbl_B 解參考. 雖然對指標解參考這樣的運算對效能影響十分小, 但是我們希望效能進一步提高. 故編碼器雖然會繼承 B 的虛擬函式表, 但是不僅僅是繼承指標, 而是將整份表加入進來 :

#include <typeinfo>

struct __virtual_table_for_C {
    std::type_info __RTTI_for_A_and_C;
    void *__vptr_f1;
    void *__vptr_f2;
    void *__vptr_f3;
    std::type_info __RTTI_for_when_static_type_is_B;
    void *__vptr_B_f2;
};

不論編碼期如何處理, 編碼器在處理完成之後必須保證在 C 的虛擬函式表中包含類別 B 的完整虛擬函式表. 對於 B tmp {new C {}} 這種建構, 虛擬函式表指標應該指向 __vptr_B_f2.

類別 C 在經過編碼器處理之後是這樣的 :

struct C {
    void *__vtbl_C;
    int a;
    void *__vtbl_B;
    int b;
    int c;
    virtual void f1() override;
    virtual void f2() override;
    virtual void f3();
};

其中, __vtbl_C 會直接指向 __virtual_table_for_C::__vptr_f2, __vtbl_B 會直接指向 __virtual_table_for_C::__vptr_B_f2. 根據文章《【C++】記憶體對位》, 在 64 位元作業系統下, sizeof(C) 的值應該是 32. 對於宣告 A tmp1 {new A {}};A tmp2 {new C {}};, this 指標會直接指向 __vtbl_C; 而宣告 B tmp3 {new C {}};this 指標則會指向 __vtbl_B.

現在將 Code 9-1C 覆寫的 f1f2 都移除掉, 其虛擬函式表將類似於 :

#include <typeinfo>

struct A {
    int a;
    virtual void f1();
};
struct B {
    int b;
    virtual void f2();
};
struct C : A, B {
    int c;
    virtual void f1() override;
    virtual void f3();
};

struct __virtual_table_for_C {
    std::type_info __RTTI_for_A_and_C;
    void *__vptr_f1;
    void *__vptr_f3;
    std::type_info __RTTI_for_when_static_type_is_B;
    void *__vptr_f2;
};

Code 9-3 相比, __virtual_table_for_C 中少了一個關於函式 f2 的指標. 這個時候 f2 相對於 C 來說就不再是一個虛擬函式, 而是普通的成員函式. 所以透過 C 來呼叫 f2 就相當於通過 C 來呼叫普通的成員函式, 並不需要虛擬函式表的幫助. 但是編碼器又不可以將 f2B 的虛擬函式表中移除. 因為編碼器無法知道是否存在其它類別繼承了 B 或者 C, 然後覆寫了來自 B 的虛擬函式 f2.

2.2 Thunk

Code 9-3 中, __vptr_f2_B__vptr_f2 有什麼區別呢? 按道理, 編碼器可以直接讓 __vptr_f2_B 指向前面的 __vptr_f2, 因為它們必定都指向 C 中重寫過的成員函式 f2. 但是實際上, __vptr_f2_B__vptr_f2 的記憶體位址是不相同的. 在編碼器處理之後, __vptr_f2_B 應該是這樣的 :

#include <typeinfo>

struct __virtual_table_for_C {
    std::type_info __RTTI_for_A_and_C;
    void *__vptr_f1;
    void *__vptr_f2;
    void *__vptr_f3;
    std::type_info __RTTI_for_when_static_type_is_B;
    void *__thunk_B_f2;       // from __vptr to __thunk
};

struct C {
    void *__vtbl_C;
    int a;
    void *__vtbl_B;
    int b;
    int c;
    virtual void f1() override;
    virtual void f2() override;
    virtual void f3();
};

那麼編碼器額外增加的 Thunk 又是什麼? 在前面的分析中, 我們知道 __vptr_f2 實際指向了 C::f2 的記憶體位址, __thunk_f2 的指向不是 C::f2, 而是指向了另外一塊記憶體. 這塊記憶體儲存著和 Thunk 相關的程式碼片段 (閣下可以簡單地理解為這塊記憶體儲存著另外有一個函式, 如果學習過 Assembly 會更容易理解), 這個程式碼片段所做的事情就是將 this 指標調整指向 __vbtl_C, 接著再透過類別 C 的虛擬函式表呼叫虛擬函式 f2. 以 B tmp {new C {}} 為例, 當 tmp 建構完成的時候, 由於其編碼期型別為 B, 所以 tmpthis 指標實際指向了 __vtbl_B, 而 __vtbl_B 指向了 __thunk_f2. 當我們透過 tmp->f2() 呼叫成員函式 f2 的時候, Thunk 首先將 this 調整指向 __vtbl_C, 然後向後偏移一個指標大小找到 __vptr_f2, 接著開始運作 C 中覆寫的成員函式 f2.

編碼器為什麼要把 B 的虛擬函式表中的 __vptr_f2 改寫為 __thunk_f2, 而不是直接將 __thunk_f2 指向 __vptr_f2 呢? 我們首先擴充 Code 9-1 :

struct A {
    int a;
    virtual void f1();
};
struct B {
    int b;
    virtual void f2();
};
struct C : A, B {
    int c;
    virtual void f1() override;
    virtual void f2() override;
    virtual void f3();
};

void f(B *p);        // 在編碼的時候, 編碼器是否知道 p 的實際型別呢?

對於函式 f 來說, 編碼器顯然無法知道接受到的引數 p 的實際型別到底是 B * 還是 C *. 對於函式呼叫 p->f1() 來說, 編碼器會將其翻譯為

  1. 尋找 p 的虛擬函式指標 __vtbl;
  2. __vtbl 解參考得到 p 的虛擬函式表 __vptr, __vptr 指向類別 B 中宣告順序為第一個的虛擬函式;
  3. 由於 f1 位於虛擬函式列表的第一個, 所以對 __vptr 向後偏移 0 位元組;
  4. 透過 __vptr 來呼叫函式.

如果不採用 Thunk, 編碼器應該直接在編碼器就給出 p 虛擬函式表中成員函式 f2 的指向. 但是在編碼的時候, 編碼器根本不知道 p 到底是 B * 還是 C *, 所以無法直接給出真實指向, 只能透過 Thunk 這樣的方式進行調整. 另外, 上面給的實例都足夠簡單, 一旦重寫過後的 f1 需要用到 C 中的成員變數, 使用直接指向 __vptr_f1 的方式會使得 C 中的 this 無法正確找到自己的成員變數, 這可能導致未定行為.

需要注意的是, 若且唯若衍生類別覆寫了基礎類別中的虛擬函式, 此時基礎類別虛擬函式表中的虛擬函式指標才會採用 Thunk. 如果衍生類別並沒有覆寫基礎類別中的虛擬函式, 那麼基礎類別虛擬函式表中的虛擬函式指標會直接指向對應的函式. 例如 Code 9-5 中的 __vptr_f2, 在類別 C 沒有覆寫虛擬函式 f2 的情況下, 是不會被改為 __thunk_f2 的.

2.3 offset_to_top

#include <typeinfo>

struct A {
    int a;
    virtual void f();
};
struct B {
    int b;
    virtual void f();
};

struct __virtual_table_for_C {
    std::type_info __RTTI_for_A_and_C;
    void *__vptr_f;
    std::type_info __RTTI_for_static_type_B;
    void *__thunk_B_f;
};

struct C : A, B {
    /* __virtual_table_for_A_and_C *__vtbl_A_and_C; */
    /* int a; */
    /* __virtual_table_for_B *__vtbl_B; */
    /* int b; */
    int c;
    virtual void f() override;
};

void f(C *p) {
    A *p1 {p};      // the virtual table pointer of p1 points to __virtual_table_for_C::__vptr_f
    B *p2 {p};      // the virtual table pointer of p2 points to __virtual_table_for_C::__thunk_f
    p1->f();        // call C::f
    p2->f();        // call C::f
}

Thunk 的作用是調整 this 指標的位置後呼叫正確的虛擬函式, 但是具體又是如何調整的呢? 在 Code 10-1 中, 需要調整 this 指標的是 p2. __virtual_table_for_C::__thunk_f 首先將 p2 中的 this 指標 (調整前它指向了 C::__vtbl_B) 暫時偏移至 __vtbl_A_and_C. 接著透過 __vtbl_A_and_C 來呼叫成員函式 f. 現實中, __vtbl_A_and_C__vtbl_B 之間不一定只有一個成員變數 a, 可能還有很多其它成員變數. 每個不同類別的偏移量都不是確定的, 所以編碼器會在虛擬函式表中插入另外一個名為 offset_to_top 的變數, 用來標記當前的 this 指標到頂部的位置. offset_to_top 的真實大小是需要考慮記憶體對位的, 所以在 Code 10-1 中, offset_to_top 的值應該是 -16 :

#include <typeinfo>

struct A {
    int a;
    virtual void f();
};
struct B {
    int b;
    virtual void f();
};
struct C : A, B {
    int c;
    virtual void f() override;
public:
    using __f_type = void ();
};

struct __virtual_table_for_B;
struct __virtual_table_for_C {
    long __offset_to_top_A_and_C {0};
    std::type_info __RTTI_for_A_and_C;
    void *__vptr_f;
    long __offset_to_top_B {-16};
    std::type_info __RTTI_for_when_static_type_is_B;
    void *__thunk_B_f;       // points to __thunk_f2_for_B
};

template <typename Class, typename F>
union __void_ptr_to_memfn_ptr {
    void *__ptr;
    F Class::*__memfn_ptr;
};

void __thunk_f2_for_B(void *this) {
    auto __vtbl_B_address {reinterpret_cast<void *>(*reinterpret_cast<unsigned long *>(this))};
    auto __offset_to_top_address_B {static_cast<char *>(__vtbl_B_address) - 2 * sizeof(long *)};
    auto __offset_to_top_value_B {*reinterpret_cast<long *>(__offset_to_top_address_B)};
    this = static_cast<char *>(this) - __offset_to_top_value_B;
    auto __vtbl_C_address {reinterpret_cast<void *>(*reinterpret_cast<unsigned long *>(this))};
    auto __vptr_f_address {*reinterpret_cast<unsigned long *>(__vtbl_C_address)};
    __void_ptr_to_memfn_ptr<C, typename C::__f_type> __ptr_cast;
    __ptr_cast.__ptr = reinterpret_cast<void *>(__vptr_f_address);
    __ptr_cast.__memfn_ptr();       // call C::f1 with this
}

struct __real_C {
    void *__vtbl_C;     // points to __virtual_table_for_C::__vptr_f1
    int a;
    void *__vtbl_B;     // points to __virtual_table_for_C::__thunk_f2
    int b;
    int c;
    virtual void f();
};

事實上, 編碼器會像 Code 10-2 那樣把 __RTTI__offset_to_top 插入到每一個多型類別的虛擬函式表頭部, 順序是 __offset_to_top 在前, __RTTI 在後. __offset_to_top 的型別不一定是 long, 但是 sizeof __offset_to_top 的值一定要和一個指標的 sizeof 值相同. 這是為了保證將虛擬函式表指標向後偏移兩個指標大小之後, 解參考後一定是 __offset_to_top 的值, 而不是其它值. 為了加深對 Thunk 的理解, Code 10-2 還模擬了 __thunk_f_B 的實作.

2.4 RTTI

Code 10-1 中, 對於 p1 的運作時型別識別信息和 p2 應該是相同的, 所以編碼器沒有必要像 Code 10-2 那樣單獨為 B 產生一份新的 __RTTI_for_static_type_B, 還要將 __RTTI_for_A_and_C 複製給它. 一般來說, 只需要讓 __RTTI_for_static_type_B 指向 __RTTI_for_A_and_C 即可. 於是我們有 :

struct __virtual_table_for_C {
    long __offset_to_top {0};
    std::type_info *__RTTI_for_A_and_C;
    void *__vptr_f;
    long __offset_to_top {-16};
    std::type_info *__RTTI_for_static_type_B {__RTTI_for_A_and_C};
    void *__thunk_B_f;
};

Code 10-3 中的虛擬函式表已經和大部分編碼器給 Code 10-1 中類別 C 生成的虛擬函式表高度相似了.

3. 虛擬繼承

在虛擬繼承體系中, 虛擬基礎類別有且僅能有一份, 我們是否能夠使用多重繼承或者多繼承的方案來解決虛擬繼承呢? 先來看一個實例, 在這個實例中我們假設編碼器仍然按照多重繼承或者多繼承的方式來實作虛擬繼承 :

#include <typeinfo>

struct A {
    int a;
    virtual void f1();
};
struct B : virtual A {
    int b;
    virtual void f1() override;
    virtual void f2();
};
struct C : virtual A {
    int c;
    virtual void f1() override;
    virtual void f3();
};
struct D : B, C {
    int d;
    virtual void f1() override;
    virtual void f2() override;
    virtual void f3() override;
    virtual void f4();
};

struct __virtual_table_for_D {
    long __offset_to_top {0};
    std::type_info *__RTTI_for_A_and_B_and_D;
    void *__vptr_f1;
    void *__vptr_f2;
    void *__vptr_f3;
    void *__vptr_f4;
    long __offset_to_top_D {-16};
    std::type_info *__RTTI_for_C;
    void *__thunk_C_f1;
    void *__thunk_C_f3;
};

struct __virtual_table_for_C;

struct __real_D {
    __virtual_table_for_D *__vtbl_A_and_B_and_D;        // points to __virtual_table_for_D::__vptr_f1
    int a;
    int b;
    __virtual_table_for_C *__vtbl_C;        // points to __virtual_table_for_D::__thunk_f1_C
    int c;
    int d;
};

Code 11-1 中, 我們將虛擬繼承類別 A 的虛擬函式表合併給 B, 然後將合併後的虛擬函式表再次合併到衍生類別最高層 D 的虛擬函式表中, 得到了 __virtual_table_for_D. D 在經過編碼器處理之後, 實際的樣子類似於 __real_D. 當靜態型別為 A 但實際型別為 D 的物件使用各種虛擬函式或者成員變數的時候, 通過 Thunk 以及 __offset_to_top 都可以正確將 this 調整到相應位置. 看起來並沒有什麼問題.

現在進一步改進 Code 11-1, 為類別 D 增加一個基礎類別 :

#include <typeinfo>

struct A {
    int a;
    virtual void f1();
};
struct B : virtual A {
    int b;
    virtual void f1() override;
    virtual void f2();
};
struct C : virtual A {
    int c;
    virtual void f1() override;
    virtual void f3();
};
struct D0 {
    virtual void f4();
};
struct D : D0, B, C {
    int d;
    virtual void f1() override;
    virtual void f2() override;
    virtual void f3() override;
    virtual void f4() override;
};

編碼器在合併虛擬函式表的時候, 會將 D0D 的虛擬函式表合併, 接下來逐漸添加 BC 的虛擬函式表. 問題在於, 虛擬基礎類別 A 的虛擬函式表應該被放在哪裡? 如果不是虛擬繼承, 那麼 D 中就會存在兩份關於類別 A 的虛擬函式表, 分別被合併到 BC 兩者的虛擬函式表中. 但是現在 AD 中只能存在一份, 也就是只能存在一份虛擬函式表, 最終應該合併給 B 還是 C? 不妨假設合併給先出現在衍生列表中的 B. 對於一個接受型別為 C & 的函式, 編碼器在編碼的時候並不知道 C 的真實型別, 所以就需要找到其對應的虛擬函式表. 但是如果將虛擬函式表合併給了 B, C 中就不存在這個表了, 編碼器在偏移 this 指標的時候就會出現問題. 那將虛擬函式表合併給 C 呢? 其實也有同樣的問題.

經過上面的討論, 我們起碼能夠想到虛擬基礎類別的虛擬函式表不應該被合併到任意一個類別的虛擬函式表中, 那麼現在有且唯有一種解決方案 : 在衍生類別中像 Code 11-1 __real_D 中的 __vtbl_C 那樣獨立存在一份. 雖然我們想到了正確的解決方案, 但是新的問題也隨之而來 : 虛擬基礎類別的虛擬函式表應該放在類別的起始位置, 中間位置, 還是最後?

3.1 vbase_offset

為了解決虛擬基礎類別的虛擬函式表的放置問題, 首先要解決如何讓衍生類別的 this 正確偏移到虛擬基礎類別的問題. 由於虛擬基礎類別只能在繼承體系中存在一份, 所以不論虛擬基礎類別被放在什麼位置, 其對於不同的衍生類別, 偏移量也是不一樣的. 不妨假設將虛擬基礎類別的虛擬函式表放在衍生類別的頭部 :

struct base;
struct A : virtual base;
struct B : virtual base;
// ...
struct derived : A, B, ...;

struct __virtual_table_for_derived;
struct __virtual_table_for_virtual_base_class;
struct __virtual_table_for_A;
struct __virtual_table_for_B;
// ...

struct __virtual_table_for_derived_class {
    __virtual_table_for_virtual_base_class *__vtbl_base;
    __virtual_table_for_derived *__vtbl_derived;
    __virtual_table_for_A *__vtbl_A;
    __virtual_table_for_B *__vtbl_B;
    // ...
};

void f(base &);

對於靜態型別為 A 或者 B, 而實際型別為 derived 的物件來說, 它們偏移到 __vtbl_base 的距離是不同的. 而編碼時, 編碼器並不知道函式 f 接收到的引數真實型別是什麼. 所以對於虛擬繼承體系來說, 還需要一個額外的標識變數, 用來表示 this 從當前型別轉換到虛擬基礎類別型別所需要的偏移量, 即 vbase_offset. vbase_offset 一般會被放在 offset_to_top 的前面 :

#include <typeinfo>

struct __virtual_table_for_virtual_base_class {
    long __vbase_offset;
    long __offset_to_top;
    std::type_info *__RTTI;
    // ...
};

現在我們來詳細討論虛擬基礎類別的虛擬函式表的具體位置. 假設虛擬基礎類別的虛擬函式表被放在了衍生類別的最前面, 即 :

#include <typeinfo>

struct A {
    int a;
    virtual void f1();
};
struct B : virtual A {
    int b;
    virtual void f2();
};
struct C : virtual A {
    int c;
    virtual void f1() override;
    virtual void f3();
};
struct D : B, C {
    int d;
    virtual void f2() override;
    virtual void f3() override;
    virtual void f4();
};

struct __virtual_table_for_D {
    /* virtual A */
    long __offset_to_top_A {16};
    std::type_info *__RTTI_for_virtual_A {__RTTI_for_and_B_and_D};
    void *__thunk_A_f1;
    /* D with merged B */
    long __vbase_offset_B_and_D {-16};
    long __offset_to_top_B_and_D {0};
    std::type_info *__RTTI_for_B_and_D;
    void *__vptr_f2;
    void *__vptr_f3;
    void *__vptr_f4;
    /* C */
    long __vbase_offset_C {-32};
    long __offset_to_top_C {-16};
    std::type_info *__RTTI_for_C {__RTTI_for_and_B_and_D};
    void *__thunk_C_f1;
    void *__thunk_C_f3;
};

struct __virtual_table_for_C;
struct __virtual_table_for_virtual_A;

struct __real_D {
    __virtual_table_for_virtual_A *__vtbl_virtual_A;        // points to __virtual_table_for_D::__thunk_A_f1
    int a;
    __virtual_table_for_D *__vtbl_and_B_and_D;        // points to __virtual_table_for_D::__vptr_f1
    int b;
    __virtual_table_for_C *__vtbl_C;        // points to __virtual_table_for_D::__thunk_C_f1
    int c;
    int d;
};

void f(const A &a) {
    a.f1();
}

Code 14 中, 我們不但將 A 的虛擬函式表放在了 __virtual_table_for_D 的頭部, 還把 A 的虛擬函式表指標放在了 __real_D 的頭部. 在最後的函式 f 中, 通過動態繫結呼叫了成員函式 f1. 如果 a 的實際型別為 C, 那麼應該呼叫 C::f1; 否則, 應該直接呼叫 A::f1. 但是 __thunk_A_f1 暫時還沒辦法讓 this 向衍生類別偏移, 所以我們是不是應該在 __offset_to_top_A 之前再放置 __offset_to_derived_D__offset_to_top_C? 先不回答這個問題, 再想像一下把 A 的虛擬函式表放在 __virtual_table_for_D 中任意中間位置會怎麼樣? 無法向前偏移的時候可以借助 offset_to_top, 但是向後偏移的時候還是需要 __offset_to_derived. 那如果把 A 的虛擬函式表放在 __virtual_table_for_D 的最後呢? 此時, __offset_to_derived 就不再需要了. 所以最好的解決方案是將 A 的虛擬函式表放在 __virtual_table_for_D 的最後.

除了上面這個問題之外還有一個問題, 如果將 A 的虛擬函式表放在 __virtual_table_for_D 的頭部, 外部的函式 f 能否正確運作? 首先我們需要確定, 要找到虛擬成員函式 f1, 在多重繼承和多繼承中, 對虛擬函式表中內容的偏移量為 0. 因為多重繼承和多繼承中, A 的虛擬函式表會被合併入 BC 的虛擬函式表, 最後 B 的虛擬函式表又會被合併入 D 的虛擬函式表中. 以此為基礎, a 的實際型別有四種情況, 我們分別分析 :

  1. a 的實際型別為 A 的時候, this 指向了 __real_A::__vtbl_virtual_A, 解參考後為 __virtual_table_for_A::__vptr_f1 (注意 __virtual_table_for_A__real_ACode 14 中沒有寫出來, 讀者可以自行寫出來), 由於 A 本身是繼承體系中最低層的類別, 所以直接呼叫 A::f1 並不會出現什麼問題;
  2. a 的實際型別為 B 的時候, 儘管 B 是虛擬衍生自 A, 但是這並不會改變 B 的虛擬函式表佈局 (這個事實在當前小節成立, 但是將在第 3.2 節中打破), 其佈局還是和多重繼承和多繼承類似. 此時 AB 的虛擬函式表是合併的, 所以可以直接透過虛擬函式表來呼叫 f1, 這並不會出現什麼問題;
  3. a 的實際型別為 C 的時候, 情況類似於第二種情形;
  4. a 的實際型別為 D 的時候, 雖然 BD 的虛擬函式表是合併的, 但是虛擬基礎類別 A 的虛擬函式表並不在其中, 而是獨立出來了. 所以在表中偏移 0 位元找到的函式是 B::f2, 而不是 A::f1. 不論 A::f1 是實際的虛擬函式表還是 Thunk, 這種偏移都找不到 A::f1. 因此 a.f1() 實際呼叫的是 B::f2, 出現了錯誤.

綜上所述, A 的虛擬函式表應該被放在 D 虛擬函式表的最後. 第二個問題才是 A 被確定放在最後的核心原因. 值得一提的是, 雖然 C++ 標準並沒有規定編碼器應該如何實現多型和動態繫結, 但是 C++ 標準卻直接規定了虛擬基礎類別的虛擬函式表應該被放在最後. 這也是為了記憶體佈局的一致性.

#include <typeinfo>

struct A {
    int a;
    virtual void f1();
};
struct B : virtual A {
    int b;
    virtual void f2();
};
struct C : virtual A {
    int c;
    virtual void f1() override;
    virtual void f3();
};
struct D : B, C {
    int d;
    virtual void f2() override;
    virtual void f3() override;
    virtual void f4();
};

struct __virtual_table_for_D {
    /* D with merged B */
    long __vbase_offset_B_and_D {32};
    long __offset_to_top_B_and_D {0};
    std::type_info *__RTTI_for_and_B_and_D;
    void *__vptr_f2;
    void *__vptr_f3;
    void *__vptr_f4;
    /* C */
    long __vbase_offset_C {16};
    long __offset_to_top_C {-16};
    std::type_info *__RTTI_for_C {__RTTI_for_and_B_and_D};
    void *__thunk_C_f1;
    void *__thunk_C_f3;
    /* virtual A */
    long __offset_to_top_A {-32};
    std::type_info *__RTTI_for_virtual_A {__RTTI_for_and_B_and_D};
    void *__thunk_A_f1;
};

struct __virtual_table_for_C;
struct __virtual_table_for_virtual_A;

struct __real_D {
    __virtual_table_for_D *__vtbl_and_B_and_D;        // points to __virtual_table_for_D::__vptr_f1
    int b;
    __virtual_table_for_C *__vtbl_C;        // points to __virtual_table_for_D::__thunk_C_f1
    int c;
    int d;
    __virtual_table_for_virtual_A *__vtbl_virtual_A;        // points to __virtual_table_for_D::__thunk_A_f1
    int a;      // move to the last
};

上面的實例中, 虛擬基礎類別 A 的虛擬函式表在衍生類別中一直是獨立存在的, 但是有一種情況比較特殊, 衍生類別的虛擬函式表將會和虛擬基礎類別的虛擬函式表合併 (這是編碼器優化的結果), 那就是 A 沒有非靜態成員變數的情形 :

#include <typeinfo>

struct A {
    virtual void f1();
};
struct B : virtual A {
    int b;
    virtual void f2();
};
struct C : virtual A {
    int c;
    virtual void f1() override;
    virtual void f3();
};
struct D : B, C {
    int d;
    virtual void f2() override;
    virtual void f3() override;
    virtual void f4();
};

struct __virtual_table_for_D {
    /* D with merged A and B */
    long __vbase_offset_A_and_B_and_D {0};
    long __offset_to_top_A_and_B_and_D {0};
    std::type_info *__RTTI_for_A_and_B_and_D;
    void *__vptr_f1;
    void *__vptr_f2;
    void *__vptr_f3;
    void *__vptr_f4;
    /* C */
    long __vbase_offset_C {-16};
    long __offset_to_top_C {-16};
    std::type_info *__RTTI_for_C {__RTTI_for_A_and_B_and_D};
    void *__vptr_C_f1;
    void *__thunk_C_f3;
};

struct __virtual_table_for_C;
struct __virtual_table_for_virtual_A;

struct __real_D {
    __virtual_table_for_D *__vtbl_and_B_and_D;        // points to __virtual_table_for_D::__vptr_f1
    int b;
    __virtual_table_for_C *__vtbl_C;        // points to __virtual_table_for_D::__thunk_C_f1
    int c;
    int d;
    __virtual_table_for_virtual_A *__vtbl_virtual_A;        // points to __virtual_table_for_D::__thunk_A_f1
};

需要注意的是, 雖然 A 的虛擬函式表和 BD 的虛擬函式表進行了合併, 但是 D 中關於 A 的虛擬函式表指標還是位於 D 的記憶體佈局的最後. 這也是編碼器為了遵循 C++ 標準而做出的妥協.

3.2 vcall_offset

在上面的討論中, 我們其實忽略了一個情況 : 當透過虛擬基礎類別呼叫衍生類別中覆寫的虛擬函式時, 由於虛擬基礎類別被排列在衍生類別的最後, 當衍生類別覆寫了虛擬基礎類別中的虛擬函式時, this 向中間層類別的偏移量是編碼期無法確定的. 比如在 Code 15__virtual_table_for_D::__thunk_f1_A 中, 我們需要將 this 偏移至類別 C 部分. 假如 BD 也覆寫了 f1, 那麼 this 也可能向 BD 偏移. 這個時候, this 的真實偏移量是通過運作時物件的真實型別來確定的. 因此我們還需要一個額外的標識 vcall_offset, 來標記在呼叫虛擬基礎類別中的虛擬函式時, this 向真實型別的偏移量應該是多少. 由於虛擬基礎類別的虛擬函式表中並不存在 vbase_offset, 所以 vcall_offset 會被放在虛擬基礎類別的虛擬函式表中 offset_to_top 之前.

#include <typeinfo>

struct A {
    int a;
    virtual void f1();
};
struct B : virtual A {
    int b;
    virtual void f2();
};
struct C : virtual A {
    int c;
    virtual void f1() override;
    virtual void f3();
};
struct D : B, C {
    int d;
    virtual void f2() override;
    virtual void f3() override;
    virtual void f4();
};

struct __virtual_table_for_D {
    /* D with merged B */
    long __vbase_offset_B_and_D {32};
    long __offset_to_top_B_and_D {0};
    std::type_info *__RTTI_for_and_B_and_D;
    void *__vptr_f2;
    void *__vptr_f3;
    void *__vptr_f4;
    /* C */
    long __vbase_offset_C {16};
    long __offset_to_top_C {-16};
    std::type_info *__RTTI_for_C {__RTTI_for_and_B_and_D};
    void *__thunk_C_f1;
    void *__thunk_C_f3;
    /* virtual A */
    long __vcall_offset_f1 {-16};
    long __offset_to_top_A {-32};
    std::type_info *__RTTI_for_virtual_A {__RTTI_for_and_B_and_D};
    void *__thunk_A_f1;
};

struct __virtual_table_for_C;
struct __virtual_table_for_virtual_A;

struct __real_D {
    __virtual_table_for_D *__vtbl_and_B_and_D;        // points to __virtual_table_for_D::__vptr_f1
    int b;
    __virtual_table_for_C *__vtbl_C;        // points to __virtual_table_for_D::__thunk_C_f1
    int c;
    int d;
    __virtual_table_for_virtual_A *__vtbl_virtual_A;        // points to __virtual_table_for_D::__thunk_A_f1
    int a;      // move to the last
};

我們在第 3.1 節中分析為什麼虛擬基礎類別的虛擬函式表應該放在最後的第二個問題中提到, 在第 3.1 節中我們可以暫時認為當某個類別 T 虛擬繼承自基礎類別的時候, 其虛擬函式表的佈局和多繼承與多重繼承是相同的. 但實際上, 哪怕沒有其它類別從 T 衍生, T 的虛擬函式表也和多重繼承與多繼承的虛擬函式表不同 :

#include <typeinfo>

struct A {
    int a;
    virtual void f1();
};
struct B : virtual A {
    int b;
    virtual void f2();
};

struct __virtual_table_for_B {
    /* D */
    long __vbase_offset_B {16};
    long __offset_to_top_B {0};
    std::type_info *__RTTI_for_and_B;
    void *__vptr_f2;
    /* virtual A */
    long __vcall_offset_f1 {0};
    long __offset_to_top_A {-16};
    std::type_info *__RTTI_for_virtual_A {__RTTI_for_and_B};
    void *__vptr_f1;
};

struct __virtual_table_for_virtual_A;

struct __real_B {
    __virtual_table_for_B *__vtbl_B;
    int b;
    __virtual_table_for_virtual_A *__vtbl_virtual_A;
    int a;
};

這麼做主要是為了保持虛擬基礎類別在衍生類別記憶體佈局中的一致性.

vbase_offset 的數量並非固定的, 而是根據衍生類別虛擬函式表中關於虛擬基礎類別那部分, 有多少個 Thunk, 就會有多少個 vbase_offset. 而且這些 vbase_offset 的排列和虛擬函式的宣告順序是相反的. 這主要是因為每一個 Thunk 對於 this 的偏移量都是不固定的 :

#include <typeinfo>

struct A {
    int a;
    virtual void f1();
    virtual void g();
    virtual void h();
};
struct B : virtual A {
    int b;
    virtual void f2();
};
struct C : virtual A {
    int c;
    virtual void f1() override;
    virtual void f3();
};
struct D : B, C {
    int d;
    virtual void f2() override;
    virtual void f3() override;
    virtual void f4();
};

struct __virtual_table_for_D {
    /* D with merged B */
    long __vbase_offset_B_and_D {32};
    long __offset_to_top_B_and_D {0};
    std::type_info *__RTTI_for_and_B_and_D;
    void *__vptr_f2;
    void *__vptr_f3;
    void *__vptr_f4;
    /* C */
    long __vbase_offset_C {16};
    long __offset_to_top_C {-16};
    std::type_info *__RTTI_for_C {__RTTI_for_and_B_and_D};
    void *__thunk_C_f1;
    void *__thunk_C_f3;
    /* virtual A */
    long __vcall_offset_h {0};
    long __vcall_offset_g {0};
    long __vcall_offset_f1 {-16};
    long __offset_to_top_A {-32};
    std::type_info *__RTTI_for_virtual_A {__RTTI_for_and_B_and_D};
    void *__thunk_A_f1;
    void *__vptr_g;
    void *__vptr_h;
};

第 3.1 節中, 我們提到了虛擬基礎類別沒有非靜態成員函式的情形. 在這種情況下, vbase_offset 的位置會稍微有些不同 :

#include <typeinfo>

struct A {
    virtual void f1();
};
struct B : virtual A {
    int b;
    virtual void f2();
};
struct C : virtual A {
    int c;
    virtual void f1() override;
    virtual void f3();
};
struct D : B, C {
    int d;
    virtual void f2() override;
    virtual void f3() override;
    virtual void f4();
};

struct __virtual_table_for_D {
    /* D with merged A and B */
    long __vbase_offset_A_and_B_and_D {0};
    long __vcall_offset_A_and_B_and_D {0};
    long __offset_to_top_A_and_B_and_D {0};
    std::type_info *__RTTI_for_A_and_B_and_D;
    void *__vptr_f1;
    void *__vptr_f2;
    void *__vptr_f3;
    void *__vptr_f4;
    /* C */
    long __vbase_offset_C {-16};
    long __vcall_offset_C {0};
    long __offset_to_top_C {-16};
    std::type_info *__RTTI_for_C {__RTTI_for_A_and_B_and_D};
    void *__vptr_C_f1;
    void *__thunk_C_f3;
};

通過 Code 20, 我們可以發現當虛擬基礎類別為空並且其虛擬函式表被合併的時候, vcall_offset 的排列位於 vbase_offsetoffset_to_top 之間. 這也是為了保持一致性, 因為其它情況下 vcall_offset 的排列就是在 offset_to_top 的前一個位置.

4. 輸出虛擬函式表

不同編碼器對虛擬函式表的處理可能有所不同, 因此最好的方式就是通過編碼旗標來輸出當前編碼器為多型類別所產生的虛擬函式表, 結合第 2 節中 Code 8 那樣的方式再將地址輸出, 進一步加深對多型類別記憶體佈局的理解.

在 Clang 中, 我們可以透過編碼旗幟 -Xclang -fdump-vtable-layouts 讓編碼器輸出多型類別的虛擬函式表佈局. 需要注意的是, 如果要查看某個類別的虛擬函式表, 必須宣告該類別的一個變數, 否則編碼器會自動優化掉, 從而導致沒有任何輸出. Clang 的編碼指令為 clang++ -Xclang -fdump-vtable-layouts 檔案存取路徑 :

Figure 1-1. clang++ -Xclang -fdump-vtable-layouts 檔案存取路徑

如果要查看多型類別的記憶體佈局, 我們可以透過 -Xclang -fdump-record-layouts 編碼旗幟, 這個指令不需要宣告類別對應的變數. 具體的指令為 clang++ -Xclang -fdump-record-layouts 檔案存取路徑 (合併指令為 clang++ -Xclang -fdump-vtable-layouts -Xclang -fdump-record-layouts 檔案存取路徑) :

Figure 1-2. clang++ -Xclang -fdump-record-layouts 檔案存取路徑

在 GCC 8 之後, 我們可以透過編碼旗幟 -fdump-lang-class 來查看虛擬函式表的佈局. 在 GCC 8 之前, 編碼旗幟為 -fdump-class-hierarchy. 和 Clang 不同的是, GCC 並不需要宣告類別變數, 而且輸出的是一個以 .class 結尾的檔案. 具體的編碼指令為 gcc -fdump-lang-class 檔案存取路徑 :

Figure 2-1. gcc -fdump-lang-class 檔案存取路徑

另外, 編碼旗幟 -fdump-lang-class 可以直接輸出多型類別的記憶體佈局, 不像 Clang 用兩條指令去處理 :

Figure 2-2. gcc -fdump-lang-class 檔案存取路徑