33

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

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

我认识到了错误并解决了我的问题,但我仍然有一个烦人的问题:VTT 到底是什么?

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

4

2 回答 2

50

“GCC C++ Compiler v4.0.1 中多重继承的注意事项”页面现已下线,http: //web.archive.org未归档。因此,我在tinydrblog找到了文本的副本,该文本存档在 web archive中。

原始注释的全文作为“博士编程语言研讨会:GCC 内部原理”(2005 年秋季)的一部分在线发表,由圣路易斯华盛顿大学计算机科学系分布式对象计算实验室的研究生 Morgan Deters 发表。 "
他的(存档)主页

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

摩根威慑网页:

第1部分:

基础知识:单一继承

正如我们在课堂上所讨论的,单一继承会导致对象布局,其中基类数据位于派生类数据之前。因此,如果类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_offsettypeinfo 指针位于 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 的指针的函数,一切就绪:它可以B::w()通过(第二个)vtable 指针进行调用,并通过指针访问成员 b我们以与通过任何指向 B 的指针相同的方式传递。

请注意,调用方法也需要进行这种“指针校正”。在这种情况下,类C继承。B::w()w()通过pointer-to-C调用时,w()需要调整指针(也就是里面的this指针。这就是常说的this指针调整。

在某些情况下,编译器会生成一个 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()在 的实例上调用时,就会调用 thunk。Cthunk 有什么作用?让我们拆开它(这里,用gdb):

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

所以它只是调整this指针并跳转到C::w()。一切都很好。

但是上面不是意味着B's vtable 总是指向这个C::w()thunk 吗?我的意思是,如果我们有一个指向 B 的指针,它合法地是 a B(不是 a C),我们不想调用 thunk,对吧?

正确的。上述用于 in 的嵌入式 vtable 对于BB C-in-C 情况是特殊的。B的正则vtable是正常的,直接指向B::w()

The 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,那么我们就无法摆脱C在 a中嵌入 a 的“技巧” D(并为 in 的 vtable 的一部分C嵌入vtable)。但是如果我们不能这样做,我们如何处理通常的类型替换呢?DD

让我们尝试绘制布局图:

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

于 2013-04-19T04:06:42.377 回答
15
THIS IS THE TEXT by Morgan Deters and NOT CC-licensed. 

摩根威慑网页:

第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的构造A函数通过改变它的 vtable 指针来修改对象的一部分!它是如何知道将这种 B-in-D 与 B-in-something-else(或B就此而言的独立)区分开来的?简单的。虚拟表表告诉它这样做。这种结构,缩写为VTT,是用于构造的 vtable 表。在我们的例子中,VTTD看起来像这样:

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。

控制权返回给 D 构造函数,它调用 C 构造函数(带有指向“C-in-D+12”条目的 VTT 地址参数)。当 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 的 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 的析构函数以与构造期间相同的方式操作 vtable 指针;也就是说,相关的 vtable 指针现在指向 C-in-D 构造 vtable。然后它运行用户对 C 的销毁代码并将控制权返回给 D 的析构函数,它接下来调用 B 的析构函数并引用 D 的 VTT。B 的析构函数设置对象的相关部分以引用 B-in-D 构造 vtable。它运行用户对 B 的销毁代码并将控制权返回给 D 的析构函数,最终调用 A 的析构函数。一个' s 析构函数将对象 A 部分的 vtable 更改为引用 A 的 vtable。最后,控制权返回到 D 的析构函数,对象的析构完成。对象曾经使用过的内存返回给系统。

现在,事实上,这个故事有点复杂。您是否曾在 GCC 生成的警告和错误消息或 GCC 生成的二进制文件中看到那些“负责”和“不负责”的构造函数和析构函数规范?好吧,事实是可以有两个构造函数实现和最多三个析构函数实现。

“负责”(或完整对象)构造函数是构造虚拟基础的构造函数,而“不负责”(或基础对象)构造函数则不是。考虑我们上面的例子。如果构造了一个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

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

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

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()在我们构建它时返回。

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

Thunks,虚拟和非虚拟

这部分还没写。

一侧具有虚拟方法的多重继承

好的。最后一个练习。如果我们像以前一样有一个带有虚拟继承的菱形继承层次结构,但只有一侧有虚拟方法呢?所以:

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 的所有实例都有一个空的 vtable。

谢谢,摩根威慑!!

于 2013-04-19T04:13:40.293 回答