GCC-3.4.6源代码学习笔记(142)

时间:2022-09-10 10:24:09

5.12.5.2.2.2.1.3.12.       完成派生类RECORD_TYPE – 生成VTT

虚表表(VTT)对于类来说不是必需的,因此下面的build_vtt可能生成VTT,有可能不产生。注意下面5188行的dump_class_hierarchy,选项“–fdump-class-hierarchy”会促使该函数转储我们前一个看到的内容。

 

finish_struct_1 (continue)

 

5174     /* Build the VTTfor T.  */

5175     build_vtt (t);

5176  

5177     if (warn_nonvdtor &&TYPE_POLYMORPHIC_P (t) && TYPE_HAS_DESTRUCTOR (t)

5178        && DECL_VINDEX(TREE_VEC_ELT (CLASSTYPE_METHOD_VEC (t), 1)) == NULL_TREE)

5179       warning ("`%#T' has virtual functionsbut non-virtual destructor", t);

5180  

5181     complete_vars (t);

5182  

5183     if (warn_overloaded_virtual)

5184       warn_hidden (t);

5185  

5186     maybe_suppress_debug_info (t);

5187  

5188     dump_class_hierarchy (t);

5189    

5190     /* Finish debuggingoutput for this type.  */

5191     rest_of_type_compilation (t, ! LOCAL_CLASS_P(t));

5192   }

 

对于包含虚拟基类的类,上面构建的vtable还不是最终成品。它需要虚表表来代替虚表。一个VTT包含了:

1. 主虚指针,用于最后派生类(the most derived class)完整对象。

2. 次要VTT,用于每个要求VTT的最后派生类的直接非虚拟基类(direct non-virtual base)。

3. 次要虚指针,用于包含虚拟基类的最后派生类的直接或间接基类,或在虚拟派生路径上的基类。

4. 次要VTT,用于每个最后派生类的直接或间接虚拟基类。

次要VTT类似于完整对象的VTT,除了没有第四部分。

关于VTT及派生类布局,这里有一个相当好的笔记,摘录如下(原文是英文)

基础:单继承

正如我们在关于类的讨论,单继承引致一个基类数据布置在在派生类数据之前的对象布局。因此如果类AB有如此定义:

class A {

public:

  int a;

};

class B : public A {

public:

  int b;

};

那么类型的对象被布局成这样(其中“b”是一个指向这样一个对象的指针):

GCC-3.4.6源代码学习笔记(142)

如果我们有虚函数:

class A {

public:

  int a;

  virtual void v();

};

class B : public A {

public:

  int b;

};

那么我们还将有一个vtable指针:

GCC-3.4.6源代码学习笔记(142)                        

也就是说,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的对象被布置成如下:

GCC-3.4.6源代码学习笔记(142)

..但是为什么?为什么有两个vtable?好吧,考虑类型替代。如果我有一个指向C的指针,我可以把它传给一个期望一个指向A的指针的函数,或一个期望指向B的指针的函数。如果一个函数期望一个指向A的指针,并且我想向它传递我的变量c(指向C的类型)的值,我已经设置好了。对A::v()的调用可以通过(第一个)vtable实现,并且被调用的函数可以通过我传入的指针访问成员,与通过指向A的指针那样。

不过,如果我向一个期望指向B的指针的函数传入我的指向c的指针变量的值,为了引用它,我们也需要在我们的C里有一个类型B的子对象。这就是为什么我们具有第二个vtable指针。我们可以向期望指向B的指针的函数传入该指针的值(c + 8 bytes),并且它是所需的设置:它可以通过这个(第二个)vtable指针进行调用B::w(),并且访问通过我们传入的指针访问成员b,与通过指向B的指针那样。

注意到被调用函数也需要这个“指针更正“(pointer-correction)。类C继承类B::w()属于这个情况。当通过指向C的指针调用w()时,这个指针(在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现在看起来象这样:

GCC-3.4.6源代码学习笔记(142)

现在,当通过指向B的指针在一个C实例上调用w()时,这个thunk被调用。这个thunk起什么作用呢?让我们反汇编它(这里,用gdb):

0x0804860c <_ZThn8_N1C1wEv+0>:  addl   $0xfffffff8,0x4(%esp)

0x08048611 <_ZThn8_N1C1wEv+5>:  jmp    0x804853c <_ZN1C1wEv>

那么它仅调整这个this指针并跳到C::w()。一切都没问题。

但上面不是意味着Bvtable总是指向这个C::w() thunk吗?我是说,如果我们有一个B的指针指向B(而不是C),我们不想调用这个thunk,对吧?

对的。上面C中嵌入的用于Bvtable是特定于这个C中的B的情况。B的正常vtable是通常形式的,并且直接指向B::w()

菱形层次:基类的多个拷贝(非虚拟继承)

OK。现在要解决真正麻烦的东西。回忆当形成继承菱形时,基类多个拷贝通常遇到的问题:

class A {

public:

  int a;

  virtual voidv();

};

class B :public A {

public:

  int b;

  virtual voidw();

};

class C :public A {

public:

  int c;

  virtual voidx();

};

class D :public B, publicC {

public:

  int d;

  virtual voidy();

};


注意到DBC继承。而BC都从A继承。这意味着D具有A两个拷贝。对象的布局及嵌入的vtable,我们可以依据前一节推导出:

GCC-3.4.6源代码学习笔记(142)

显然,我们期望A的数据(成员a)在D的对象布局中出现两次(它就是如此),并且我们期望A的虚拟成员函数在vtable表示两次(A::v()确实如此)。OK,这里没有什么新玩意。

菱形层次:虚拟基类的单个拷贝

但是如果我们虚拟继承会怎样呢?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();

};

一下子事情变得复杂多了。如果我们可以在我们的D的表达中仅拥有A的一份拷贝,那么我们可以不再依赖我们的“小技巧”把C嵌入到一个D中(并且在Dvtable中嵌入一个用于DC部分的vtable)。不过如果我们不能做到这一点,我们怎样可以处理普通的类型替代呢?

让我们尝试把布局画出来:

GCC-3.4.6源代码学习笔记(142)

OK。你看到A现在嵌入在D中,基本上就像其它基类那样。不过它被嵌入在D中,而不是在它的直接派生类中。

多继承情况下的构造与析构

当上面的对象被构造时,对象如何在内存中被构造?并且我们如何确保构造函数在一个部分构造的对象(及它的vtable)上的操作是安全的?

幸运的是,这些都得到了非常小心的处理。比如说我们正在构建类型D的一个新对象(通过比如,new D)。首先,用于该对象的内存在堆上分配并且返回一个指针。D的构造函数被调用,但在执行任何D特定的构造前,在对象上调用A的构造函数(当然,在调整了this指针之后)。A的构造函数填充类D对象的A部分,就像它是A的一个实例。

GCC-3.4.6源代码学习笔记(142)

控制权交还给D的构造函数,它调用B的构造函数( 在这里指针调整是不需要的)。当B的构造函数做完后,该对象看起来就像这样:

GCC-3.4.6源代码学习笔记(142)

但等一下... B的构造函数修改了该对象中的A部分,它改变Avtable指针!怎么能把这种的B-in-D与其他中的B(或者一个单独的B)区分开来呢?简单。虚表表告诉它这样做。这个结构,缩写为VTT,是一个vtable的表,在构造中使用。在我们的案例中,用于DVTT看起来就像这样:

GCC-3.4.6源代码学习笔记(142)

D的构造函数把DVTT中的一个指针传入B的构造函数(在这种情况下,它传入了第一个B-in-D项的地址)。确实,这个用于具有上面布局的对象的vtable是仅用于构造这个B-in-D的特殊vtable

控制权返回给D的构造函数,接着它调用C的构造函数(连同一个指向VTT项“C-in-D+12”地址的参数)。当C的构造函数完成时,该对象看起来就像这样:

GCC-3.4.6源代码学习笔记(142)

正如你所见,C的构造函数再一次修改了嵌入的Avtable指针。这个嵌入的CA对象现在使用这个C-in-D vtable的特殊构造,并且嵌入的B对象使用这个B-in-D vtable的特殊构造。最后,D的构造函数完成了这个工作,我们得到与之前相同的图:

GCC-3.4.6源代码学习笔记(142)

析构函数以相同的方式但反序执行。D的析构函数被调用。用户的析构代码运行后,该析构函数调用C的析构函数,并且指导它使用DVTT的相关部分。C的析构函数,以在构造过程中相同的方式,操纵这个vtable指针;就是说,这个vtable指针现在指向C-in-D的构造vtableconstruction vtable)。然后运行用户的用于C的析构代码,并且把控制权返回给D的析构函数,它接着调用B的析构函数连同D中的VTT的一个引用。B的析构函数设置对象的相关部分来引用B-in-D的构造vtableconstruction vtable)。运行用户用于B的析构代码,并且把控制权返回给D的析构函数,它最后调用A的析构函数。A的析构函数改变用于对象A部分的vtable来引用用于Avtable。最后,控制权返回给D的析构函数,对象的析构完成。曾用于该对象的内存返回给系统。

现在,事实上,事情还要更复杂些。你是否曾经看到那些“in-charge”及“not-in-charge”规格的构造函数及析构函数,在GCC产生的警告及错误消息或GCC生成的2进制文件中?是的,事实是可以有2个构造函数的实现,以及多达3个的析构函数的实现。

一个“in-charge”(或者完整对象)构造函数是会构造虚拟基类的,而一个“not-in-charge”(或基类对象)构造函数则不会。考虑我们上面的例子。如果一个B被构造,它的构造函数需要调用A的构造函数来。类似的,C的构造函数需要构造A。然而,如果BC作为D的一个构造的一部分来构造,它们的构造函数不应该构造A,因为A是一个虚拟基类,并且D的构造函数将担负起在D的实例中仅构造它一次的责任。考虑这些情况:

·         如果你执行“new A”,A的“in-charge”构造函数被调用来构造A

·         当你执行“new B”,B的“in-charge”构造函数被调用。它将调用A的“not-in-charge”构造函数。

·         new C”类似于“new B”。

·         new D”调用D的“in-charge”构造函数。我们浏览这个例子。D的“in-charge”构造函数调用ABC not-in-charge”版本的构造函数(以这个次序)。

一个“in-charge”析构函数类似于一个“in-charge”构造函数——它负责析构虚拟基类。类似的,有“not-in-charge”析构函数产生。但这里还有第三个。一个“in-charge deleting”析构函数除了析构对象之外,还负责回收内存。那么什么时候其中一个会被选中使用呢?

首先,有两类对象可以被析构——在栈上分配的,及在堆上分配的。考虑这个代码(假设使用之前我们具有虚拟继承的菱形派生结构):

D d;            // 在栈上分配一个D并构造它

D *pd = new D;  // 在堆上分配一个D并构造它

/* ... */

delete pd;      // D调用“in-charge deleting”析构函数

return;         // 为栈分配的D调用“in-charge”析构函数

我们看到实际的delete操作符没有由执行删除的代码来调用,而是由用于要被删除对象的“in-charge deleting”析构函数来调用。为什么要这样做?为什么不让这个调用者调用“in-charge”析构函数,然后删除这个对象呢?那样你只需要析构函数的2个实现,而不是3...

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

D *pd = new D;  // 在堆上分配一个D并构造它

C *pc = d;      // 我们有一个C的指针指向我们堆上分配的D

/* ... */

delete pc;      // 通过vtable调用析构函数的thunk,但对于删除?

如果你没有D的析构函数的一个“in-charge deleting”形式,那么删除操作将需要调整指针,就像这个析构函数thunk做的那样。记住,C对象嵌入在一个D里,因此我们上面的C指针被调整指向我们D对象的内部。我们不能就这样删除这个指针,因为它不是那个当我们构造D时由malloc()返回的指针。

因此,如果我们没有一个“in-charge deleting”析构函数,我们不得不对删除操作符使用thunk(并把它们保存在我们的vtable中),或其他类似的东西。

多继承,一边具有虚函数

OK。最后一个练习。如果我们有一个具有虚拟继承的菱形继承层次,就像之前那样,但仅在一边有虚函数,会怎样呢?这样:

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

};

在这个情形下对象的布局如下:

GCC-3.4.6源代码学习笔记(142)

你可以看到C子对象,它没有虚函数,但仍然有一个vtable(尽管是空的)。事实上,所有C的实例都有一个空的vtable