最近遇到了一个对我来说很新的C ++链接器错误。
libfoo.so: undefined reference to `VTT for Foo'
libfoo.so: undefined reference to `vtable for Foo'
我认识到这个错误并解决了我的问题,但我还有一个唠叨的问题:VTT究竟是什么?
旁白:对于那些感兴趣的人,当你忘记定义一个类中声明的第一个虚函数时会出现问题。 vtable进入类的第一个虚函数的编译单元。如果你忘了定义那个函数,你会得到一个链接器错误,它无法找到vtable而不是更加开发人员友好的找不到该函数。
答案 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:
基础:单一继承
正如我们在课堂上讨论的那样,单继承导致一个对象布局,其基类数据在派生类数据之前布局。因此,如果定义了类
A
和B
: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,对吗?右。
B
中C
的上述嵌入式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
继承自B
和C
,而B
和C
都继承自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 =虚拟表格。
答案 5 :(得分:1)
虚拟表表(缩写为VTT)是一个vtable表,用于构造中涉及多重继承。
这篇有趣的文章中提供了更多信息:Notes on Multiple Inheritance in GCC C++ Compiler v4.0.1