【问题标题】:virtual function and modified this pointer虚函数并修改了this指针
【发布时间】:2016-02-22 01:25:07
【问题描述】:

考虑下面的代码

class B1 {
public:
  void f0() {}
  virtual void f1() {}
  int int_in_b1;
};

class B2 {
public:
  virtual void f2() {}
  int int_in_b2;
};


class D : public B1, public B2 {
public:
  void d() {}
  void f2() {int temp=int_in_b1;}  // override B2::f2()
  int int_in_d;
};

以及对象 d 的以下内存布局:

d:
  +0: pointer to virtual method table of D (for B1)
  +4: value of int_in_b1
  +8: pointer to virtual method table of D (for B2)
 +12: value of int_in_b2
 +16: value of int_in_d

Total size: 20 Bytes.

virtual method table of D (for B1):
  +0: B1::f1()  // B1::f1() is not overridden

virtual method table of D (for B2):
  +0: D::f2()   // B2::f2() is overridden by D::f2()

D  *d  = new D();

d->f2();

d->f2();被调用时,D::f2需要访问来自B1的数据,但是修改了这个指针

(*(*(d[+8]/*pointer to virtual method table of D (for B2)*/)[0]))(d+8) /* Call d->f2() */

传给D::f2,那么D::f2怎么能访问呢?

代码来自:https://en.wikipedia.org/wiki/Virtual_method_table#Multiple_inheritance_and_thunks

【问题讨论】:

  • 我不确定您所说的“修改此指针”是什么意思。一切运行良好,因为 D 中同时包含 B1 和 B2。当 D::f2 生成时,它知道如何从 this 中访问 int_in_b1 的 ussign 偏移量。
  • @SergeyA codepad.org/4DIgwoMe 查看输出。
  • 我之前发过一些很蠢的cmets,请见谅。
  • 我不清楚你在这里实际问的是什么。您是否有一些行为不端的代码,或者您只是在询问事情是如何工作的?
  • @EJP 我只是在问事情是如何运作的。

标签: c++ multiple-inheritance vtable abi virtual-functions


【解决方案1】:

您的情况实际上太简单了:编译器可以知道您有一个指向D 对象的指针,因此它可以从右表执行查找,将未修改的this 指针传递给f2() 实现.


有趣的情况是,当你有一个指向 B2 的指针时:

B2* myD = new D();
myD->f2();

现在我们从调整后的基指针开始,需要为整个对象找到this 指针。实现这一点的一种方法是在函数指针旁边存储一个偏移量,该函数指针用于从用于访问 vtable 的B2 指针生成一个有效的this 指针。

因此,在您的情况下,代码可能会像这样隐式编译

D* myD = new D();
((B2*)myD)->f2();

调整指针两次(一次从D* 导出B2*,然后使用vtable 的偏移量进行反向)。不过,您的编译器可能足够聪明,可以避免这种情况。

无论如何,这完全属于实施领域。你的编译器可以做任何事情,只要它表现符合标准规定的方式。

【讨论】:

  • 对于每个虚函数,每个基类指针都应该有(base x, offset)可以调用它?我对么?在使用虚拟继承的菱形继承中会出现上述情况。
  • 使用我草拟的方法,每个虚函数都需要一对函数指针和this 偏移量。该对将驻留在首先引入虚函数的基础的 vtable 中。如果引入的基类是虚拟基类,则需要首先解析指向该虚拟基类的 this 指针,就像在静态情况下所做的那样。
  • ideone.com/UXRHZ6 在这种情况下,derived::fun() 可以从base1 *intermediate * 调用,并且需要不同的偏移量。
  • 当我在您的示例类层次结构中执行myIntermediate->fun() 时,编译器可以首先隐式执行虚拟基解析base1* converted = myIntermediate;,然后继续生成converted->fun(); 的代码。也就是说,编译器只需要从单个 vtable 中查找 fun() 的对。当然,它还需要能够解析一个类的所有虚基的位置。我想这是派生类的 vtable 中每个虚拟基础的另一个条目。 vtables 的这些虚拟基础条目将与函数指针完全不同。
  • vtable 将有两种不同的条目:1. 虚拟基本偏移量,以及 2. 对(虚拟函数指针,this-adjustment 的偏移量)。 1. 将用于base* converted = myIntermediate;,2. 将用于converted->fun();。 1. 将是每个虚拟基类,2. 将是每个虚拟函数。这能说明问题吗?
【解决方案2】:

首先,您描述为“修改this 指针”的效果是某些特定编译器的实现细节。没有特定要求编译器像您描述的那样修改指针。

也没有要求对象具有 vtables,更不用说它们像您描述的那样布局。实际要求是在运行时调用正确的虚函数重载,并且能够正确访问数据成员和调用成员函数。现在,在实践中,编译器倾向于使用 vtables,但这是一个实现细节,因为各种措施的替代方案效率较低。

现在,也就是说,下面的讨论将假设每个具有虚函数的类都有一个 vtable。看看你的例子,这是做什么的?

D  *d  = new D();

d->f2();

首先,编译器知道d是一个指向D的指针,并且知道D有一个名为f2()的函数。它还将知道f2() 是一个从B2 继承的虚函数(这是除非编译器能够看到完整的类定义,否则无法调用类成员函数的原因之一)。

在这种情况下,我们知道dD 是什么,所以我们知道应该调用D::f2(),并且this 指针的值等于d。编译器具有相同的信息(它知道dD *)所以它就是这样做的。现在,好吧,它可能会或可能不会在 vtable 中查找 D::f2(),但这就是它的结尾。

更有趣的例子,就像 cmaster 说的,是

B2* myD = new D();
myD->f2();

在这种情况下,myD 是指向B2 的指针。编译器知道B2 有一个名为f2() 的虚函数,因此知道它必须调用正确的重载。

问题是,在myD->f2() 语句中,编译器可能不知道myD 实际上指向D(例如,对象的构造和成员函数的调用可能在不同的函数中,在不同的编译单元中)。但是,它确实知道B2 有一个名为f2() 的虚函数,它是正确调用实际重载版本所必需的。

这意味着编译器需要两位信息。首先,它需要识别要调用的实际函数 (D::f2()) 的信息。第二点信息将是对myD 的一些调整,以使D::f2() 的调用正常工作。这第二位信息本质上是从myD 生成(您所称的)“修改后的this 指针”所需要的。

如果编译器在 vtable 的帮助下完成所有这些操作,它可能会在 vtable 中包含 B2 的两个信息位。所以(假设第二位信息是偏移量)编译器转

myD->f2();

变成类似的东西

(myD + myD->vtable->offset_for_f2)->(myD->vtable->entry_for_f2)();

(myD + myD->vtable->offset_for_f2) 部分本质上就是您所描述的“修改后的this 指针”,D::f2() 在调用时会看到它。 (myD->vtable->entry_for_f2) 部分本质上是D::f2() 的地址(比如成员函数的地址)。

下一个要问的问题是编译器如何填充 vtable?简短的回答是它在构造对象时执行此操作。

B2* myD = new D();

新表达式 (new D()) 本质上扩展为

void *temp = ::operator new(sizeof (D));    // assuming class does not supply its own operator new

//  construct a `D` in the memory pointed to by temp

temp = (D *)myD;    // the compiler knows we're creating a D, so doesn't use offsets or anything funky here

将指向temp的内存变成D的过程很重要。首先调用基类的构造函数(B2B2),然后构造或初始化Ds的成员,然后调用D的构造函数(C++标准实际上对事件的顺序做了非常详细的描述)。另一件事是编译器会记账以确保我们实际上从进程中获得了有效的D。其中一部分是填充 vtable。

现在,由于编译器对类 D 的定义具有完整的可见性(即基类、其成员等的完整定义),因此它具有填充 vtable 所需的所有信息。换句话说,它拥有为myD->vtable->offset_for_f2myD->vtable->entry_for_f2 提供合理值所需的所有信息

在多重继承的情况下,假设每个基类有一个 vtable,编译器拥有以类似方式填充所有 vtable 所需的所有信息。换句话说,编译器知道它如何在内存中布置对象,包括它们的 vtable,并适当地使用这些知识。

但是,话又说回来,它可能不会。正如我所说,vtables 是编译器中常用的一种技术,用于实现/支持虚函数调度。还有其他方法。

【讨论】:

    【解决方案3】:

    再次,我无法发表评论,所以必须在这里回答。

    代码没有问题!

    D::f2 需要访问来自B1 的数据

    那么D::f2 是如何访问它的呢?

    只需写入D::f2,B1::int_in_b1,然后您就可以访问 int 值。

    【讨论】:

    • 其实,OP 甚至不需要用B1::int_in_b1 来限定,OP 可以简单地说int tmp = int_in_b1 就可以了。
    • 在他的情况下,是的,它不需要它!但我真的不知道他需要回答他什么!此代码按预期工作!在多态中,正确的写与 OP。
    • 我读这个问题的方式,不是关于如何编写可以工作的 C++ 代码,而是关于编译器如何从这个 C++ 代码生成一个可以工作的可执行文件。
    【解决方案4】:

    在您的示例中,当调用 d->f2() 时,编译器知道 d 是指向 D 类的指针。要调用 f2(),它将在传递之前将 d 的指针调整为 B2 的“this”正如你所描述的,它是虚拟的 f2()。现在,在 D::f2() 内部,编译器知道这是 D::f2() 并且它知道 D 如何从 B2 继承,因此它将 B2 的“this”固定为 D 的“this”在函数的最开始,所以当你的代码执行时,它会看到“this”是 D 的。因此它可以访问 D::f2() 内的 D 的任何成员。 如果你有

    B2* b = d;
    b->f2();
    

    当调用 b->f2() 时,传递给 f2() 的指针是 B2 的“this”。在 D::f2() 内部,传递的指针固定指向 D 的 this。

    【讨论】:

      猜你喜欢
      • 2021-12-17
      • 1970-01-01
      • 2014-03-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多