我从here读到了调整器thunk。这是一些引用:
现在,只有一个QueryInterface 方法,但有两个条目,一个 为每个vtable。记住每个 vtable中的函数接收 对应的接口指针为其 “这个”参数。这很好 QueryInterface(1);它的界面 指针与对象的相同 界面指针。但这是个坏消息 对于QueryInterface(2),因为它 接口指针是q,而不是p。
这是调节器雷击的来源 英寸
我想知道为什么“ vtable中的每个函数都会收到相应的接口指针作为其”this“参数”?它是接口方法用于在对象实例中定位数据成员的唯一线索(基址)吗?
这是我最新的理解:
事实上,我的问题不是关于 此 参数的目的,而是关于为什么我们必须使用相应的接口指针作为 这个 参数。对不起我的含糊不清。
除了在对象的布局中使用界面指针作为 定位器/立足点 之外。只要你是组件的实现者,当然还有其他方法可以做到这一点。
但对于我们组件的客户来说情况并非如此。
当组件以COM方式构建时,组件的客户端对组件的内部结构一无所知。 客户端只能抓住接口指针,这是将作为 this 参数传递给接口方法的指针。在期望下,编译器别无选择,只能根据此此 指针生成接口方法的代码 。
因此上述推理导致结果:
必须确保每项功能 在一个vtable必须收到 对应的接口指针为其 “ 此 ”参数。
对于“this pointer adjustor thunk”,单个QueryInterface()方法存在2个不同的条目,换句话说,可以使用2个不同的接口指针来调用QueryInterface()方法,但编译器只生成QueryInterface()方法的1个副本。因此,如果编译器选择其中一个接口作为this指针,我们需要将另一个调整为所选择的接口。这就是这个调整器thunk诞生的原因。
BTW-1,如果编译器可以生成2个不同的QueryInterface()方法实例怎么办?每一个都基于相应的接口指针。这不需要调整器thunk,但是需要更多空间来存储额外但相似的代码。
BTW-2:似乎有时问题从实施者的角度来看缺乏合理的解释,但可以从用户的指针视角更好地理解。答案 0 :(得分:11)
从问题中删除COM部分,this
指针调整器thunk是一段代码,用于确保每个函数都获得指向具体类型子对象的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; }
};
(并且忽略了缺少初始化)。 base
中的derived
子对象未与对象的开头对齐,因为 [1] 之间存在offset
。当指向derived
的指针被转换为指向base
的指针(包括隐式转换,但不会重新解释会导致UB和潜在死亡的转换)时,指针的值会被偏移,以便{{1} }对于(void*)d != (void*)((base*)d)
类型的假定对象d
。
现在考虑一下用法:
derived
当从derived d;
base * b = &d; // This generates an offset
b->bar();
b->foo();
指针或引用调用函数时出现问题。如果虚拟调度机制发现最终的覆盖者在base
中,则指针base
必须引用this
对象,如base
中隐含的{{}} 1}}指针与b->bar
中存储的地址相同。现在,如果最终覆盖在派生类中,与this
一样,b
指针必须与找到最终覆盖的类型的子对象的开头对齐(在本例中为{ {1}})。
编译器所做的是创建一段中间代码。调用虚拟调度机制时,在调度到b->foo()
之前,中间调用采用this
指针并将偏移量减去derived
对象的开头。此操作与向下转换derived::foo
相同。请记住,此时this
指针的类型为derived
,因此它最初是偏移的,这实际上会返回原始值static_cast<derived*>(this)
。
[1] 即使在 interfaces 的情况下也存在偏移 - 在Java / C#意义上:仅定义虚拟方法的类 - 如他们需要将一个表存储到该接口的vtable。
答案 1 :(得分:3)
Here's来自其中一位设计师的关于MSVC内部的文章。它解释了MSVC实现的许多其他细节。您可能还想查看我在OpenRCE上关于它在装配中的外观的文章。
答案 2 :(得分:0)
它是接口方法用于在对象实例中定位数据成员的唯一线索(基址)吗?
是的,这就是它的全部内容。
答案 3 :(得分:0)
是的,this
对于找到对象的起始位置至关重要。你写下你的代码:
variable = 10;
其中variable
是成员变量。首先,它属于哪个对象?它属于this
指针指向的对象。所以它实际上是
this->variable = 10;
现在C ++需要生成能够实现工作的代码 - 复制数据。为了做到这一点,它需要知道对象start和成员变量之间的偏移量。惯例是this
始终指向对象的开始,因此偏移量可以是常量:
*(reinterpret_cast<int*>( reinterpret_cast<char*>( this ) + variableOffset ) ) = 10; //assuming variable is of type int
答案 4 :(得分:0)
我认为重要的是要指出在C ++中没有“接口指针”这样的实体或者接近它的任何东西。它最多建立在受限制的抽象类的概念上,但仍然是一个类。因此,适用于班级成员和处理“此”的所有规则仍然适用不变。 所以主要是接口类必须表现为给定类型的独立类,而不管它们的功能和最终的继承层次结构。
我们可以使用虚方法调用机制来获取(接口)基类公开的对象的实际(动态类型)。它是如何完成的是具体实现,包括虚拟方法表和“调整器thunks”等概念。通常编译器可以使用其初始的'this'指针来定位VMT,然后使用给定函数的实际实现,并通过最终调整'this'指针来调用它。如果基类的内存布局不同于在多重继承的情况下我们持有引用的派生布局,则执行最终调用通常需要thunk调整。