什么是班级的VTT?

时间:2011-06-06 22:04:18

标签: c++ gcc vtable virtual-inheritance vtt

最近遇到了一个对我来说很新的C ++链接器错误。

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

我认识到这个错误并解决了我的问题,但我还有一个唠叨的问题:VTT究竟是什么?

旁白:对于那些感兴趣的人,当你忘记定义一个类中声明的第一个虚函数时会出现问题。 vtable进入类的第一个虚函数的编译单元。如果你忘了定义那个函数,你会得到一个链接器错误,它无法找到vtable而不是更加开发人员友好的找不到该函数。

6 个答案:

答案 0 :(得分:41)

页面"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.

Morgan Deters网页:

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,方式与通过任何指向A的指针相同。

     

但是,如果我将指针变量c的值传递给期望指向B的函数,我们还需要在C中使用类型B的子对象来引用它。这就是为什么我们有第二个vtable指针。我们可以将指针值(c + 8个字节)传递给期望指向B的函数,并且它全部设置:它可以通过(第二个)vtable指针调用B::w() ,并通过我们传递的指针访问成员b的方式与通过任何指向B的指针相同。

     

注意这个"指针修正"也需要为被调用的方法发生。在这种情况下,类C会继承B::w()。当通过指向C的指针调用w()时,需要调整指针(它成为w()内的指针。这通常称为指针调整。

     

在某些情况下,编译器会生成一个thunk来修复地址。请考虑与上述相同的代码,但这一次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的对象布局和vtable现在看起来像这样:

                           +-----------------------+
                           |     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()    |
                           +-----------------------+
     

现在,当通过指向B的w()实例调用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(而不是C)的指针,我们不想调用thunk,对吗?

     

右。 BC的上述嵌入式vtable对于B-in-C案例是特殊的。 B的常规视力表是正常的,直接指向B::w()

     

Diamond:基类的多个副本(非虚拟继承)

     

好。现在要解决真正困难的问题。回想一下在形成继承菱形时基类的多个副本的常见问题:

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继承自BC,而BC都继承自A。这意味着D中包含A的两个副本。对象布局和vtable嵌入是我们对前面部分的期望:

                           +-----------------------+
                           |     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的数据(成员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的一个副本,那么我们就无法再使用我们的&#34;技巧&#34;在C中嵌入D(并在C的vtable中嵌入D D部分的vtable。但是,如果我们不能这样做,我们如何处理通常的类型替换?

     

让我们尝试绘制布局图:

                                   +-----------------------+
                                   |   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中而不是直接派生的类中。

答案 1 :(得分:13)

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

Morgan Deters网页:

2部分:

  

存在多重继承的构造/破坏

     

当构造对象本身时,如何在内存中构造上述对象?我们如何确保部分构造的对象(及其vtable)对于构造函数的操作是否安全?

     

幸运的是,我们都非常谨慎地处理了这些问题。假设我们正在构建D类型的新对象(例如,通过new D)。首先,在堆中分配对象的内存并返回指针。 D的构造函数被调用,但在进行任何D特定构造之前,它会在对象上调用A的构造函数(在调整之后)当然是this指针!)。 A的构造函数填充A对象的D部分,就像它是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的构造函数通过更改它的vtable指针来修改对象的A部分!如何区分这种B-in-D与B-in-something-else(或者单独的B)?简单。 虚拟表格表告诉它执行此操作。这个结构缩写为 VTT ,是一个用于构造的vtable表。在我们的例子中,D的VTT如下所示:

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的构造函数(在这种情况下,它传入第一个B-in-D条目的地址)。事实上,上面用于对象布局的vtable是一个特殊的vtable,仅用于构建B-in-D。

     

Control返回到D构造函数,并调用C构造函数(VTT地址参数指向&#34; C-in-D + 12&#34;条目)。当C&#39的构造函数完成对象时,它看起来像这样:

 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的vtable指针。嵌入的C和A对象现在使用特殊构造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&#39的析构函数以与构造过程中相同的方式操纵vtable指针;也就是说,相关的vtable指针现在指向C-in-D构造vtable。然后它运行用户对C的销毁代码,并将控制权返回给D的析构函数,然后Dtructor将引用D&amp; VTT引用B的析构函数。 B的析构函数设置对象的相关部分以引用B-in-D构造vtable。它运行用户对B的破坏代码并将控制权返回给D的析构函数,后者最终调用A的析构函数。一个析构函数将对象的A部分的vtable更改为引用到A的vtable。最后,控制返回到D的析构函数并完成对象的销毁。对象使用的内存将返回给系统。

     

现在,事实上,这个故事有点复杂。你见过那些&#34;负责人&#34;和&#34;不负责&#34; GCC生成的警告和错误消息中或GCC生成的二进制文件中的构造函数和析构函数规范?嗯,事实是可以有两个构造函数实现和最多三个析构函数实现。

     

&#34;负责人&#34; (或完整对象)构造函数是构造虚拟基础的构造函数,而不是“不负责”的构造函数。 (或基础对象)构造函数是没有的。考虑我们上面的例子。如果构造了B,它的构造函数需要调用A的构造函数来构造它。类似地,C的构造函数需要构造A.但是,如果B和C构造为D的构造的一部分,则它们的构造函数不应构造A,因为A是虚拟基础和D的构造函数将为D的实例完成一次构建它。考虑案例:

     

如果你做一个新的A,A&#34; s&#34;负责&#34;构造函数被调用来构造A.   当你做一个新的B,B&#34;负责&#34;构造函数被调用。它将调用&#34;不负责&#34; A的构造函数。

     

新C类似于新B。

     

新的D调用D&#34;负责人&#34;构造函数。我们通过这个例子进行了研究。 D&#34;负责人&#34;构造函数调用&#34; not-in-charge&#34; A&C,B&C和C&C的构造函数(按此顺序)。

     

&#34;负责人&#34;析构函数是一个&#34;主管&#34;构造函数的类似物 - 它负责破坏虚拟基础。同样,&#34;不负责&#34;析构函数生成。但也有第三个。 &#34;负责删除&#34;析构函数是释放存储以及破坏对象的析构函数。那么什么时候一个人优先考虑另一个呢?

     

嗯,有两种可以被破坏的对象 - 在堆栈上分配的对象,以及在堆中分配的对象。考虑一下这段代码(假设我们的钻石层次结构具有之前的虚拟继承):

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
     

我们看到实际的删除操作符不是由执行删除的代码调用的,而是由被删除对象的负责删除析构函数调用的。为什么这样?为什么不让调用者调用负责的析构函数,然后删除对象?那么你只有两个析构函数实现副本而不是三个...

     

好吧,编译器可以做这样的事情,但由于其他原因它会更加复杂。考虑一下这段代码(假设你总是使用虚拟析构函数,对吧?......对吗?!):

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?
     

如果您没有&#34;负责删除&#34;各种D的析构函数,然后删除操作需要像析构函数thunk一样调整指针。记住,C对象嵌入在D中,所以我们指向上面的指针C被调整为指向D对象的中间。我们不能删除这个指针,因为它不是't&t; t <{1}}构造它时返回的指针。

     

因此,如果我们没有负责删除析构函数,我们必须有删除操作符的thunk(并在我们的vtable中表示它们),或其他类似的东西。

     

Thunks,Virtual and Non-Virtual

     

此部分尚未编写。

     

单面使用虚拟方法进行多重继承

     

好。最后一个练习。如果我们像以前一样拥有带有虚拟继承的钻石继承层次结构,但只在其一侧有虚拟方法,那该怎么办?所以:

malloc()
     

在这种情况下,对象布局如下:

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();
};
     

所以你可以看到没有虚方法的C子对象仍然有一个vtable(尽管是空的)。实际上,C的所有实例都有一个空的vtable。

谢谢,Morgan Deters !!

答案 2 :(得分:5)

虚拟表格表(VTT)。它允许在存在多个继承时安全地构造/解构对象。

有关解释,请参阅: http://www.cse.wustl.edu/~mdeters/seminar/fall2005/mi.html

答案 3 :(得分:2)

它们是虚拟(函数)表,以及处理多重继承的虚拟类型表。


CF:
    http://www.codesourcery.com/archives/cxx-abi-dev/msg00077.html     http://www.cse.wustl.edu/~mdeters/seminar/fall2005/mi.html

答案 4 :(得分:2)

VTT =虚拟表格。

请参阅C++ Class Representations

答案 5 :(得分:1)

虚拟表表(缩写为VTT)是一个vtable表,用于构造中涉及多重继承。

这篇有趣的文章中提供了更多信息:Notes on Multiple Inheritance in GCC C++ Compiler v4.0.1