为什么我们需要“this指针调整thunk”?

8

我从这里了解了关于adjustor thunk的信息。以下是一些引述:

现在,只有一个QueryInterface方法,但有两个入口,每个vtable都有一个。请记住,vtable中的每个函数都将对应的接口指针作为其“this”参数接收。对于QueryInterface(1)来说,它的接口指针与对象的接口指针相同,这很好。但对于QueryInterface(2)来说,情况就不一样了,因为它的接口指针是q,而不是p。

这就是adjustor thunk发挥作用的地方。

我想知道为什么vtable中每个函数都将对应的接口指针作为其“this”参数接收?这是接口方法在定位对象实例内的数据成员时所使用的唯一线索(基地址)吗?

更新

这是我最新的理解:

实际上,我的问题不是关于这个参数的目的,而是为什么我们必须使用相应的接口指针作为这个参数。抱歉我表达得不清楚。
除了在对象的布局中使用接口指针作为定位器/支撑点之外,当然还有其他方法可以做到这一点,只要你是组件的实现者。
但对于我们组件的客户端来说,情况并非如此。
当组件以COM方式构建时,我们组件的客户端对我们组件的内部一无所知。客户端只能掌握接口指针,这正是将作为this参数传递到接口方法中的精确指针。在这种期望下,编译器别无选择,只能基于这个特定的this指针生成接口方法的代码。
因此,以上推理导致结果是:

必须确保vtable中的每个函数都接收相应的接口指针作为其“this”参数。

在“this指针调整thunk”的情况下,单个QueryInterface()方法存在两个不同的条目,换句话说,可以使用2个不同的接口指针来调用QueryInterface()方法,但编译器只会生成1份QueryInterface()方法。因此,如果编译器选择其中一个接口作为this指针,我们需要将另一个接口调整为所选接口。这就是this adjustor thunk的作用。

顺便说一下-1,如果编译器可以生成2个不同的QueryInterface()方法实例呢?每个方法基于相应的接口指针。这就不需要adjustor thunk,但需要更多的空间来存储额外但相似的代码。

顺便说一下-2:有时候问题从实现者的角度缺乏合理的解释,但从用户的角度来看可能更容易理解。

4个回答

12

除去问题中的COM部分,this指针调整器是一段代码,确保每个函数都得到一个指向具体类型子对象的this指针。在多重继承中会出现问题,因为基类和派生类对象未对齐。

考虑以下代码:

struct base {
   int value;
   virtual void foo() { std::cout << value << std::endl; }
   virtual void bar() { std::cout << value << std::endl; }
};
struct offset {
   char space[10];
};
struct derived : offset, base {
   int dvalue;
   virtual void foo() { std::cout << value << "," << dvalue << std::endl; }
};

(而且忽略缺乏初始化) derived 中的 base 子对象与对象的起始位置不对齐,因为它们之间有一个 offset[1]。 当将指向 derived 的指针转换为指向 base 的指针(包括隐式转换,但不包括会引起 UB 和潜在死亡的重新解释转换)时,指针的值会被偏移,使得假设类型为 derived 的对象 d 满足 (void*)d != (void*)((base*)d)

现在考虑用法:

derived d;
base * b = &d; // This generates an offset
b->bar();
b->foo();
当从指向基类的指针或引用调用函数时,问题就出现了。如果虚拟分派机制发现最终覆盖者在基类中,则指针this必须引用基类对象,例如b->bar,其中隐式指针this是存储在b中的相同地址。如果最终覆盖者在派生类中,如b->foo(),则this指针必须与找到最终覆盖者的类型的子对象的开头对齐(在本例中为derived)。编译器所做的是创建一个中间代码段。当调用虚拟分派机制并在转到派生类的foo之前,中间调用获取this指针并减去到derived对象开头的偏移量。该操作与下面的downcast相同:static_cast(this)。请记住,在这一点上,this指针是base类型的,因此最初会产生偏移,并且这实际上返回了原始值&d。
[1] 即使是Java / C#意义上定义仅虚拟方法的类-接口,也需要存储该接口的vtable表格,因此在这种情况下也存在偏移量。

0

是的,this 对于找到对象的起始位置至关重要。在您的代码中编写:

variable = 10;

其中variable是成员变量。首先,它属于哪个对象?它属于由this指针指向的对象。因此,实际上是这样的。

this->variable = 10;

现在C++需要生成能够实际执行工作的代码 - 复制数据。为此,它需要知道对象开始和成员变量之间的偏移量。惯例是this始终指向对象开头,因此偏移量可以是常数:
*(reinterpret_cast<int*>( reinterpret_cast<char*>( this ) + variableOffset ) ) = 10; //assuming variable is of type int

谢谢您的回答。我已经更新了我的帖子。非常感谢任何评论。 - smwikipedia

0
这是接口方法用来定位对象实例中的数据成员的唯一线索(基地址)吗?
是的,确实只有这些。

谢谢你的回答。我更新了我的帖子并提出了一些想法,我很想听听任何评论。谢谢。 - smwikipedia

0

我认为需要指出的是,在 C++ 中没有“接口指针”或任何类似的实体。它最多是一种惯用语,建立在受限制的抽象类概念上,但仍然保留了类的特性。因此,所有适用于类成员和处理“this”的规则仍然适用。

因此,接口类必须像给定类型的独立类一样运作,无论其功能和继承层次结构如何。

我们可以使用虚方法调用机制来获得由(接口)基类公开的对象的实际(动态类型)。如何实现这一点是特定于实现的,包括诸如虚方法表和“调整程序”之类的概念。通常编译器可能使用其初始的“this”指针来定位 VMT,然后调用给定函数的实际实现,并以最终调整“this”指针的方式进行调用。如果基类的内存布局与我们所持有的引用的派生类不同,就像多重继承的情况一样,则通常需要进行 thunk 调整以执行最终调用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接