2011-06-06 89 views
23

最近遇到了一個新的C++鏈接器錯誤。一堂課的VTT是多少?

libfoo.so: undefined reference to `VTT for Foo' 
libfoo.so: undefined reference to `vtable for Foo' 

我意識到錯誤並解決了我的問題,但我仍然有一個嘮叨的問題:VTT到底是什麼?

旁白:對於那些有興趣的人,當你忘記定義在類中聲明的第一個虛函數時會出現問題。 vtable進入班級第一個虛擬功能的編譯單元。如果你忘記定義該函數,你會得到一個鏈接器錯誤,它找不到vtable,而不是開發人員友好的找不到該函數。

+5

@AlokSave,不是該問題的重複,問題是關於縮寫,可能對自己有用。 – unkulunkulu 2013-04-19 08:20:57

回答

2

VTT =虛擬表的表。

請參閱C++ Class Representations

+3

我相信你的意思是鏈接到http://www.cse.wustl.edu/~mdeters/seminar/fall2005/mi.html – 2011-06-06 22:41:30

+0

我沒有,但這是一個更好的鏈接:)。 – MGwynne 2011-06-06 22:57:39

+2

我沒有看到任何一個鏈接,你只是顯得很孤獨而且不合適。 – 2011-06-06 23:01:15

34

"Notes on Multiple Inheritance in GCC C++ Compiler v4.0.1"頁面現在處於脫機狀態,而http://web.archive.org沒有存檔。所以,我找到了tinydrblog的文本副本,這個文件存檔爲at the web archive

在聖路易斯的華盛頓大學計算機科學系的分佈式對象計算實驗室中,存在原始筆記的全文,其作爲「Doctoral Programming Language Seminar: GCC Internals」(2005年秋季)由畢業生Morgan Deters在線發表。「
His (archived) homepage

THIS IS THE TEXT by Morgan Deters and NOT CC-licensed. PART1: 

基礎:單繼承

正如我們在類討論的,單繼承導致與佈局導出之前類數據的基礎類數據的對象的佈局。因此,如果類AB被定義正是如此:

class A { 
public: 
    int a; 

};

class B : public A { 
public: 
    int b; 
}; 

然後B類型的對象被佈局是這樣的(其中 「B」 是指向這樣的對象):

b --> +-----------+ 
     |  a  | 
     +-----------+ 
     |  b  | 
     +-----------+ 

如果有虛擬方法:

class A { 
public: 
    int a; 
    virtual void v(); 
}; 

class B : public A { 
public: 
    int b; 
}; 

那麼你也會有一個vtable指針:

      +-----------------------+ 
          |  0 (top_offset) | 
          +-----------------------+ 
b --> +----------+   | ptr to typeinfo for B | 
     | vtable |-------> +-----------------------+ 
     +----------+   |   A::v()  | 
     |  a |   +-----------------------+ 
     +----------+ 
     |  b | 
     +----------+ 

top_offset,並且typeinfo指針位於vtable指針指向的位置的上方。

簡單的多重繼承

現在考慮多重繼承:

class A { 
public: 
    int a; 
    virtual void v(); 
}; 

class B { 
public: 
    int b; 
    virtual void w(); 
}; 

class C : public A, public B { 
public: 
    int c; 
}; 

在這種情況下,C類的對象佈局是這樣的:

      +-----------------------+ 
          |  0 (top_offset) | 
          +-----------------------+ 
c --> +----------+   | ptr to typeinfo for C | 
     | vtable |-------> +-----------------------+ 
     +----------+   |   A::v()  | 
     |  a |   +-----------------------+ 
     +----------+   | -8 (top_offset) | 
     | vtable |---+  +-----------------------+ 
     +----------+ |  | ptr to typeinfo for C | 
     |  b | +---> +-----------------------+ 
     +----------+   |   B::w()  | 
     |  c |   +-----------------------+ 
     +----------+ 

...但爲什麼?爲什麼兩個vtable在一個?那麼,考慮類型替換。如果我有一個指向C的指針,我可以將它傳遞給一個函數,該函數需要一個指向A的指針或一個期望指向B的函數。如果一個函數需要一個指向A的指針,並且我想將它傳遞給我的變量c(指向C的指針)的值,那麼我已經設置好了。可以通過(第一個)vtable調用A::v(),並且被調用的函數可以通過我通過的指針訪問成員a,方式與通過任何指針指向的方式相同。但是,如果我將指針變量c的值傳遞給需要指向B的函數,我們還需要C中的一個子對象B來引用它。這就是爲什麼我們有第二個vtable指針。我們可以將指針值(c + 8字節)傳遞給需要指針指向B的函數,並且它已全部設置:它可以通過(第二個)vtable指針調用B::w(),並通過我們以同樣的方式傳遞指針,它可以通過指向B的指針傳遞。

請注意,這個「指針校正」也需要針對被調用的方法進行。在這種情況下,類C繼承B::w()。當w()通過C指針被調用時,指針(它變成w()裏面的這個指針需要調整,這通常叫做這個指針調整)

在某些情況下,編譯器會生成修復地址。考慮上述相同的代碼,但這個時候C覆蓋B的成員函數w()

class A { 
public: 
    int a; 
    virtual void v(); 
}; 

class B { 
public: 
    int b; 
    virtual void w(); 
}; 

class C : public A, public B { 
public: 
    int c; 
    void w(); 
}; 

C的對象佈局現在虛函數表是這樣的:

      +-----------------------+ 
          |  0 (top_offset) | 
          +-----------------------+ 
c --> +----------+   | ptr to typeinfo for C | 
     | vtable |-------> +-----------------------+ 
     +----------+   |   A::v()  | 
     |  a |   +-----------------------+ 
     +----------+   |   C::w()  | 
     | vtable |---+  +-----------------------+ 
     +----------+ |  | -8 (top_offset) | 
     |  b | |  +-----------------------+ 
     +----------+ |  | ptr to typeinfo for C | 
     |  c | +---> +-----------------------+ 
     +----------+   | thunk to C::w() | 
          +-----------------------+ 

現在,當w()被稱爲通過指針B在C的實例上調用thunk。 thunk是做什麼的?讓我們來拆解(這裏,與gdb):

0x0804860c <_ZThn8_N1C1wEv+0>: addl $0xfffffff8,0x4(%esp) 
0x08048611 <_ZThn8_N1C1wEv+5>: jmp 0x804853c <_ZN1C1wEv> 

所以它只是調整this指針,並跳轉到C::w()。一切都很好。

但是,以上是否意味着B的vtable始終指向此C::w() thunk?我的意思是,如果我們有一個指向B的合法B(而不是C),我們不想調用thunk,對吧?

對。以上嵌入的用於B的的vtable對於B-in-C情況是特殊的。 B的常規虛擬表是正常的,並直接指向B::w()

鑽石:基類的多個副本(非虛擬繼承)

好。現在要解決真正困難的事情。形成一個繼承的鑽石時,回想一下基類的多個副本的通常問題:

class A { 
public: 
    int a; 
    virtual void v(); 
}; 

class B : public A { 
public: 
    int b; 
    virtual void w(); 
}; 

class C : public A { 
public: 
    int c; 
    virtual void x(); 
}; 

class D : public B, public C { 
public: 
    int d; 
    virtual void y(); 
}; 

注意D來自BCBC無論從A繼承繼承。這意味着D在其中有兩個A的副本。對象佈局和虛函數表嵌入是什麼我們會從前面的章節中預計:

      +-----------------------+ 
          |  0 (top_offset) | 
          +-----------------------+ 
d --> +----------+   | ptr to typeinfo for D | 
     | vtable |-------> +-----------------------+ 
     +----------+   |   A::v()  | 
     |  a |   +-----------------------+ 
     +----------+   |   B::w()  | 
     |  b |   +-----------------------+ 
     +----------+   |   D::y()  | 
     | vtable |---+  +-----------------------+ 
     +----------+ |  | -12 (top_offset) | 
     |  a | |  +-----------------------+ 
     +----------+ |  | ptr to typeinfo for D | 
     |  c | +---> +-----------------------+ 
     +----------+   |   A::v()  | 
     |  d |   +-----------------------+ 
     +----------+   |   C::x()  | 
          +-----------------------+ 

當然,我們希望A對象s佈局的數據(成員a)在D兩次存在'(這是),並且我們期望A的虛擬成員函數在vtable中被表示兩次(並且A::v()確實存在)。好的,這裏沒有新東西。

鑽石:如果我們將虛擬繼承虛基

但什麼的單份? C++虛擬繼承允許我們指定菱形層次結構,但只能保證虛擬繼承基礎的一個副本。所以讓我們這樣寫我們的代碼:

class A { 
public: 
    int a; 
    virtual void v(); 
}; 

class B : public virtual A { 
public: 
    int b; 
    virtual void w(); 
}; 

class C : public virtual A { 
public: 
    int c; 
    virtual void x(); 
}; 

class D : public B, public C { 
public: 
    int d; 
    virtual void y(); 
}; 

所有突然的事情變得更加複雜。如果我們只能有A一個副本,在我們的D表示,那麼我們就可以不再逃脫我們的D嵌入C(和包埋的DD的虛函數表的C部分虛函數表的「絕招」 )。但是如果我們不能這樣做,我們如何處理通常的類型替換?

讓我們試着用圖的佈局:

        +-----------------------+ 
            | 20 (vbase_offset) | 
            +-----------------------+ 
            |  0 (top_offset) | 
            +-----------------------+ 
            | ptr to typeinfo for D | 
         +----------> +-----------------------+ 
d --> +----------+ |   |   B::w()  | 
     | vtable |----+   +-----------------------+ 
     +----------+     |   D::y()  | 
     |  b |     +-----------------------+ 
     +----------+     | 12 (vbase_offset) | 
     | vtable |---------+  +-----------------------+ 
     +----------+   |  | -8 (top_offset) | 
     |  c |   |  +-----------------------+ 
     +----------+   |  | ptr to typeinfo for D | 
     |  d |   +-----> +-----------------------+ 
     +----------+     |   C::x()  | 
     | vtable |----+   +-----------------------+ 
     +----------+ |   | 0 (vbase_offset) | 
     |  a | |   +-----------------------+ 
     +----------+ |   | -20 (top_offset) | 
         |   +-----------------------+ 
         |   | ptr to typeinfo for D | 
         +----------> +-----------------------+ 
            |   A::v()  | 
            +-----------------------+ 

好。因此,您看到A現在嵌入在D中,與其他基本相同。但它嵌入在D中而不是直接派生類中。在多重繼承的存在

+0

我認爲這個vtable和VTT的基本描述應該保存在stackoverflow中。 – osgx 2013-04-19 04:13:01

+0

Morgan Deters的新主頁似乎在這裏http://cs.nyu.edu/~mdeters/ – osgx 2013-04-19 04:16:08

+0

[他的文字的第二部分在這裏](http://stackoverflow.com/a/16097089/196561) – osgx 2013-09-16 18:53:17

9
THIS IS THE TEXT by Morgan Deters and NOT CC-licensed. PART2: 

建築/銷燬

如何當對象本身的構造在內存中構造上述目的?我們如何確保部分構造的對象(及其vtable)對於構造函數的操作是安全的?

幸運的是,它的處理都非常謹慎。假設我們正在構造一個類型爲D的新對象(例如,通過new D)。首先,對象的內存被分配到堆中並返回一個指針。 D的構造函數被調用,但在執行任何D特定構造之前,它會調用對象上的A的構造函數(當然,在調整this指針後!)。 A的構造函數填充D對象的A部分,就像它是A的實例一樣。

d --> +----------+ 
     |   | 
     +----------+ 
     |   | 
     +----------+ 
     |   | 
     +----------+ 
     |   |  +-----------------------+ 
     +----------+  |  0 (top_offset) | 
     |   |  +-----------------------+ 
     +----------+  | ptr to typeinfo for A | 
     | vtable |-----> +-----------------------+ 
     +----------+  |   A::v()  | 
     | a  |  +-----------------------+ 
     +----------+ 

控制返回到D的構造,這將調用B的構造。 (此處不需要指針調整)當B的構造完成,對象是這樣的:

           B-in-D 
          +-----------------------+ 
          | 20 (vbase_offset) | 
          +-----------------------+ 
          |  0 (top_offset) | 
          +-----------------------+ 
d --> +----------+  | ptr to typeinfo for B | 
     | vtable |------> +-----------------------+ 
     +----------+  |   B::w()  | 
     | b  |  +-----------------------+ 
     +----------+  | 0 (vbase_offset) | 
     |   |  +-----------------------+ 
     +----------+  | -20 (top_offset) | 
     |   |  +-----------------------+ 
     +----------+  | ptr to typeinfo for B | 
     |   | +--> +-----------------------+ 
     +----------+ | |   A::v()  | 
     | vtable |---+ +-----------------------+ 
     +----------+ 
     | a  | 
     +----------+ 

但等待... B的構造修改的的A部分通過改變它的vtable指針來實現對象!它是如何知道區別這種B-in-D和B-in-something-else(或者是一個獨立的B)?簡單。 虛擬表格告訴它這樣做。這種結構,縮寫爲VTT,是建築中使用的vtables表。在我們的例子中,VTT爲D看起來是這樣的:

                B-in-D 
               +-----------------------+ 
               | 20 (vbase_offset) | 
      VTT for D       +-----------------------+ 
+-------------------+       |  0 (top_offset) | 
| vtable for D |-------------+   +-----------------------+ 
+-------------------+    |   | ptr to typeinfo for B | 
| vtable for B-in-D |-------------|----------> +-----------------------+ 
+-------------------+    |   |   B::w()  | 
| vtable for B-in-D |-------------|--------+ +-----------------------+ 
+-------------------+    |  | | 0 (vbase_offset) | 
| vtable for C-in-D |-------------|-----+ | +-----------------------+ 
+-------------------+    |  | | | -20 (top_offset) | 
| vtable for C-in-D |-------------|--+ | | +-----------------------+ 
+-------------------+    | | | | | ptr to typeinfo for B | 
| vtable for D |----------+ | | | +-> +-----------------------+ 
+-------------------+   | | | |  |   A::v()  | 
| vtable for D |-------+ | | | |  +-----------------------+ 
+-------------------+  | | | | | 
          | | | | |       C-in-D 
          | | | | |  +-----------------------+ 
          | | | | |  | 12 (vbase_offset) | 
          | | | | |  +-----------------------+ 
          | | | | |  |  0 (top_offset) | 
          | | | | |  +-----------------------+ 
          | | | | |  | ptr to typeinfo for C | 
          | | | | +----> +-----------------------+ 
          | | | |   |   C::x()  | 
          | | | |   +-----------------------+ 
          | | | |   | 0 (vbase_offset) | 
          | | | |   +-----------------------+ 
          | | | |   | -12 (top_offset) | 
          | | | |   +-----------------------+ 
          | | | |   | ptr to typeinfo for C | 
          | | | +-------> +-----------------------+ 
          | | |   |   A::v()  | 
          | | |   +-----------------------+ 
          | | | 
          | | |         D 
          | | |   +-----------------------+ 
          | | |   | 20 (vbase_offset) | 
          | | |   +-----------------------+ 
          | | |   |  0 (top_offset) | 
          | | |   +-----------------------+ 
          | | |   | ptr to typeinfo for D | 
          | | +----------> +-----------------------+ 
          | |    |   B::w()  | 
          | |    +-----------------------+ 
          | |    |   D::y()  | 
          | |    +-----------------------+ 
          | |    | 12 (vbase_offset) | 
          | |    +-----------------------+ 
          | |    | -8 (top_offset) | 
          | |    +-----------------------+ 
          | |    | ptr to typeinfo for D | 
          +----------------> +-----------------------+ 
           |    |   C::x()  | 
           |    +-----------------------+ 
           |    | 0 (vbase_offset) | 
           |    +-----------------------+ 
           |    | -20 (top_offset) | 
           |    +-----------------------+ 
           |    | ptr to typeinfo for D | 
           +-------------> +-----------------------+ 
               |   A::v()  | 
               +-----------------------+ 

D的構造函數將指針傳遞到D的VTT到B的構造函數(在這種情況下,通過第1 B-中的地址-D條目)。實際上,用於上述對象佈局的vtable是一個專門用於構建B-in-D的vtable。

控件返回給D構造函數,它調用C構造函數(VTT地址參數指向「C-in-D + 12」條目)。當C的構造與對象做它看起來像這樣:

                  B-in-D 
                 +-----------------------+ 
                 | 20 (vbase_offset) | 
                 +-----------------------+ 
                 |  0 (top_offset) | 
                 +-----------------------+ 
                 | ptr to typeinfo for B | 
        +---------------------------------> +-----------------------+ 
        |         |   B::w()  | 
        |         +-----------------------+ 
        |       C-in-D | 0 (vbase_offset) | 
        |  +-----------------------+ +-----------------------+ 
d --> +----------+ |  | 12 (vbase_offset) | | -20 (top_offset) | 
     | vtable |--+  +-----------------------+ +-----------------------+ 
     +----------+   |  0 (top_offset) | | ptr to typeinfo for B | 
     | b  |   +-----------------------+ +-----------------------+ 
     +----------+   | ptr to typeinfo for C | |   A::v()  | 
     | vtable |--------> +-----------------------+ +-----------------------+ 
     +----------+   |   C::x()  | 
     | c  |   +-----------------------+ 
     +----------+   | 0 (vbase_offset) | 
     |   |   +-----------------------+ 
     +----------+   | -12 (top_offset) | 
     | vtable |--+  +-----------------------+ 
     +----------+ |  | ptr to typeinfo for C | 
     | a  | +-----> +-----------------------+ 
     +----------+   |   A::v()  | 
          +-----------------------+ 

正如你看到的,C的構造函數再次修改嵌入A的虛函數表pointer.The嵌入式C,現在一個對象所使用的特殊結構C-in-D vtable,並且嵌入的B對象正在使用特殊構造的B-in-D vtable。最後,D的構造完成的工作,我們最終有相同的圖之前:以同樣的方式,但在相反的發生

        +-----------------------+ 
            | 20 (vbase_offset) | 
            +-----------------------+ 
            |  0 (top_offset) | 
            +-----------------------+ 
            | ptr to typeinfo for D | 
         +----------> +-----------------------+ 
d --> +----------+ |   |   B::w()  | 
     | vtable |----+   +-----------------------+ 
     +----------+     |   D::y()  | 
     |  b |     +-----------------------+ 
     +----------+     | 12 (vbase_offset) | 
     | vtable |---------+  +-----------------------+ 
     +----------+   |  | -8 (top_offset) | 
     |  c |   |  +-----------------------+ 
     +----------+   |  | ptr to typeinfo for D | 
     |  d |   +-----> +-----------------------+ 
     +----------+     |   C::x()  | 
     | vtable |----+   +-----------------------+ 
     +----------+ |   | 0 (vbase_offset) | 
     |  a | |   +-----------------------+ 
     +----------+ |   | -20 (top_offset) | 
         |   +-----------------------+ 
         |   | ptr to typeinfo for D | 
         +----------> +-----------------------+ 
            |   A::v()  | 
            +-----------------------+ 

破壞。 D的析構函數被調用。在用戶的銷燬代碼運行後,析構函數調用C的析構函數並指示它使用D的VTT的相關部分。 C的析構函數以與在構建過程中一樣的方式操作vtable指針;也就是說,相關的vtable指針現在指向C-in-D構建vtable。然後它運行C的用戶銷燬代碼並將控制權返回給D的析構函數,該函數接下來通過引用D的VTT來調用B的析構函數。 B的析構函數設置對象的相關部分以引用B-in-D構造vtable。它運行B的用戶銷燬代碼並將控制權返回給D的析構函數,最後調用A的析構函數。 A的析構函數將對象的A部分的vtable更改爲引用到A的vtable中。最後,控制返回到D的析構函數並完成對象的銷燬。該對象曾經使用的內存將返回給系統。其實這個故事有點複雜。你有沒有見過海灣合作委員會製作的警告和錯誤信息或海灣合作委員會製作的二進制文件中的「負責人」和「非負責人」的構造函數和析構函數規範?那麼,事實是可以有兩個構造函數實現和最多三個析構函數實現。

「負責」(或完整對象)構造函數是構造虛擬基礎的構造函數,而「不負責」(或基礎對象)構造函數是不構成的。考慮我們上面的例子。如果構造B,它的構造函數需要調用A的構造函數來構造它。同樣,C的構造函數需要構造A.但是,如果B和C構造爲D的構造的一部分,它們的構造函數不應該構造A,因爲A是虛擬基礎,D的構造函數將負責構建它對於D的實例。考慮如下情況:

如果您執行新的A,則調用A的「負責」構造函數來構造A. 當您執行新的B時,B的「負責」構造函數是調用。它會調用「非主管」構造函數A.

新的C類似於新B.

一個新的d調用D的「主管」的構造。遍歷這個例子。 D的「負責」構造函數調用A,B和C的構造函數的「不收費」版本(以此次序)。

「負責人」的析構函數是「負責」構造函數的類比 - 負責破壞虛擬基礎。類似地,生成一個「不負責」的析構函數。但還有第三個。 「負責刪除」的析構函數是釋放存儲空間並破壞對象的析構函數。那麼一個人什麼時候會優先於另一個呢?

那麼,有兩種可以被破壞的對象---在堆棧中分配的對象和在堆中分配的對象。考慮這個代碼(假設我們的鑽石等級制度與虛擬繼承自前):

D d;   // allocates a D on the stack and constructs it 
D *pd = new D; // allocates a D in the heap and constructs it 
/* ... */ 
delete pd;  // calls "in-charge deleting" destructor for D 
return;   // calls "in-charge" destructor for stack-allocated D 

我們看到,實際的delete運算符不被執行的代碼刪除調用,而是由在爲正被刪除的對象充電刪除析構函數。爲什麼這樣做?爲什麼不讓調用者調用負責的析構函數,然後刪除該對象?那麼你只有兩個析構函數的實現,而不是三個...

那麼,編譯器可以做這樣的事情,但由於其他原因,它會更復雜。考慮以下代碼(假設虛析構函數,你總是使用,正確的,對吧?!?):

D *pd = new D; // allocates a D in the heap and constructs it 
C *pc = d;  // we have a pointer-to-C that points to our heap-allocated D 
/* ... */ 
delete pc;  // call destructor thunk through vtable, but what about delete? 

如果你沒有一個「主管刪除」品種的D的析構函數,那麼刪除操作需要調整指針,就像析構函數thunk一樣。請記住,C對象嵌入到D中,因此我們將上面的指針C調整爲指向D對象的中間。我們不能只刪除此指針,因爲它不是指針當我們構建它時由malloc()返回。因此,如果我們沒有一個負責刪除析構函數,那麼我們必須要有刪除操作符(並在我們的vtable中表示它們)或其他類似的東西。

的thunk,虛擬和非虛擬

本節尚未編寫。

多重繼承,一面虛擬方法

好。最後一個練習。如果我們像以前一樣擁有一個帶有虛擬繼承的鑽石繼承層次結構,但是它只有一個虛擬方法?所以:

class A { 
public: 
    int a; 
}; 

class B : public virtual A { 
public: 
    int b; 
    virtual void w(); 
}; 

class C : public virtual A { 
public: 
    int c; 
}; 

class D : public B, public C { 
public: 
    int d; 
    virtual void y(); 
}; 

在這種情況下,對象佈局如下:

        +-----------------------+ 
            | 20 (vbase_offset) | 
            +-----------------------+ 
            |  0 (top_offset) | 
            +-----------------------+ 
            | ptr to typeinfo for D | 
         +----------> +-----------------------+ 
d --> +----------+ |   |   B::w()  | 
     | vtable |----+   +-----------------------+ 
     +----------+     |   D::y()  | 
     |  b |     +-----------------------+ 
     +----------+     | 12 (vbase_offset) | 
     | vtable |---------+  +-----------------------+ 
     +----------+   |  | -8 (top_offset) | 
     |  c |   |  +-----------------------+ 
     +----------+   |  | ptr to typeinfo for D | 
     |  d |   +-----> +-----------------------+ 
     +----------+ 
     |  a | 
     +----------+ 

所以,你可以看到C子對象,它沒有虛方法,仍然有一個vtable(雖然是空的)。事實上,C的所有實例都有一個空的虛表。

謝謝,Morgan Deters !!

+1

抄襲和複製,如果我曾經說過 – v010dya 2015-07-07 06:30:03