为什么在具有多个接口的对象中实现 QueryInterface() 时需要显式向上转换()
假设我有一个实现两个或多个 COM 接口的类:
class CMyClass : public IInterface1, public IInterface2 {
};
我看到的几乎所有文档都表明,当我为 IUnknown 实现 QueryInterface() 时,我显式地将 this 指针向上转换为其中一个接口:
if( iid == __uuidof( IUnknown ) ) {
*ppv = static_cast<IInterface1>( this );
//call Addref(), return S_OK
}
问题是为什么可以我不是只是复制这个吗?
if( iid == __uuidof( IUnknown ) ) {
*ppv = this;
//call Addref(), return S_OK
}
文档通常说,如果我执行后者,我将违反对同一对象的 QueryInterface() 的任何调用都必须返回完全相同的值的要求。
我不太明白。他们是否意味着,如果我为 IInterface2 QI() 并通过该指针调用 QueryInterface(),C++ 将传递 this 与如果我为 IInterface2 QI() 略有不同,因为 C++ 每次都会创建 this 指向子对象?
Assume I have a class implementing two or more COM interfaces:
class CMyClass : public IInterface1, public IInterface2 {
};
Almost every document I saw suggests that when I implement QueryInterface() for IUnknown I explicitly upcast this pointer to one of the interfaces:
if( iid == __uuidof( IUnknown ) ) {
*ppv = static_cast<IInterface1>( this );
//call Addref(), return S_OK
}
The question is why can't I just copy this?
if( iid == __uuidof( IUnknown ) ) {
*ppv = this;
//call Addref(), return S_OK
}
The documents usually say that if I do the latter I will violate the requirement that any call to QueryInterface() on the same object must return exactly the same value.
I don't quite get that. Do they mean that if I QI() for IInterface2 and call QueryInterface() through that pointer C++ will pass this slightly different from if I QI() for IInterface2 because C++ will each time make this point to a subobject?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
问题是
*ppv
通常是void*
- 直接将this
分配给它只会采用现有的this
> 指针并赋予*ppv
它的值(因为所有指针都可以转换为void*
)。这对于单继承来说不是问题,因为对于单继承,所有类的基指针始终相同(因为虚函数表只是针对派生类进行了扩展)。
然而,对于多重继承,您实际上最终会得到多个基指针,具体取决于您正在谈论的类的“视图”!这样做的原因是,通过多重继承,您不能只扩展 vtable - 您需要多个 vtable,具体取决于您正在讨论的分支。
因此,您需要转换
this
指针,以确保编译器将正确的基指针(对于正确的 vtable)放入*ppv
中。下面是单继承的示例:
vtable for A:
vtable for B:
请注意,如果您有
B
vtable 并且将其视为A
vtable,那么它就可以工作 -A
成员的偏移量正是您所期望的。下面是一个使用多重继承的示例(使用上面的
A
和B
的定义)(注意:只是一个示例 - 实现可能会有所不同):C 的 vtable:
D 的 vtable:
以及
D
的实际内存布局:请注意,如果您将
D
vtable 视为A
它将起作用(这是巧合 - 您可以不要依赖它)。但是 - 如果在调用c0
时将D
vtable 视为C
(编译器期望在 vtable 的槽 0 中),那么您'会突然呼叫a0
!当您在
D
上调用c0
时,编译器实际上会传递一个假的this
指针,该指针具有一个看起来应有的 vtable对于C
。因此,当您在
D
上调用C
函数时,它需要调整 vtable 以指向D
对象的中间(在D
处) >@C vtable) 在调用函数之前。The problem is that
*ppv
is usually avoid*
- directly assigningthis
to it will simply take the existingthis
pointer and give*ppv
the value of it (since all pointers can be cast tovoid*
).This is not a problem with single inheritance because with single inheritance the base pointer is always the same for all classes (because the vtable is just extended for the derived classes).
However - for multiple inheritance you actually end up with multiple base pointers, depending on which 'view' of the class you're talking about! The reason for this is that with multiple inheritance you can't just extend the vtable - you need multiple vtables depending on which branch you're talking about.
So you need to cast the
this
pointer to make sure that the compiler puts the correct base pointer (for the correct vtable) into*ppv
.Here's an example of single inheritance:
vtable for A:
vtable for B:
Note that if you have the
B
vtable and you treat it like anA
vtable it just works - the offsets for the members ofA
are exactly what you would expect.Here's an example using multiple inheritance (using definitions of
A
andB
from above) (note: just an example - implementations may vary):vtable for C:
vtable for D:
And the actual memory layout for
D
:Note that if you treat a
D
vtable as anA
it will work (this is coincidence - you can't rely on it). However - if you treat aD
vtable as aC
when you callc0
(which the compiler expects in slot 0 of the vtable) you'll suddenly be callinga0
!When you call
c0
on aD
what the compiler does is it actually passes a fakethis
pointer which has a vtable which looks the way it should for aC
.So when you call a
C
function onD
it needs to adjust the vtable to point to the middle of theD
object (at the@C
vtable) before calling the function.您正在进行 COM 编程,因此在了解
QueryInterface
为何以这种方式实现之前,需要回忆一下有关代码的一些事项。IInterface1
和IInterface2
都是IUnknown
的后代,我们假设两者都不是另一个的后代。QueryInterface(IID_IUnknown, (void**)&intf)
时,intf
将被声明为类型IUnknown*
。由于第 3 点,
QueryInterface
定义中的this
值可能会有所不同。通过IInterface1
指针调用该函数,this
将具有与通过IInterface2
指针调用时不同的值。无论哪种情况,由于第 #1 点,this
都将保存类型为IUnknown*
的有效指针,因此如果您只需分配*ppv = this
,调用者会很高兴,从 C++ 的角度来看。您将把IUnknown*
类型的值存储到相同类型的变量中(参见第 #2 点),所以一切都很好。然而,COM 比普通 C++ 拥有更强的规则。特别是,它要求对对象的 IUnknown 接口的任何请求都必须返回相同的指针,无论使用该对象的哪个“视图”来调用查询。因此,您的对象始终将
this
分配给*ppv
是不够的。有时调用者会获得IInterface1
版本,有时他们会获得IInterface2
版本。正确的 COM 实现需要确保它返回一致的结果。它通常会有一个if
-else
梯子检查所有支持的接口,但其中一个条件将检查两个接口,而不是仅一个,第二个是IUnknown
:IUnknown
检查与哪个接口分组并不重要,只要对象仍然存在时分组不会更改,但您确实必须出去实现这一目标的方法。You're doing COM programming, so there are a few things to recall about your code before looking at why
QueryInterface
is implemented the way it is.IInterface1
andIInterface2
descend fromIUnknown
, and let's assume neither is a descendant of the other.QueryInterface(IID_IUnknown, (void**)&intf)
on your object,intf
will be declared as typeIUnknown*
.QueryInterface
could be called through any one of them.Because point #3, the value of
this
in yourQueryInterface
definition can vary. Call the function via anIInterface1
pointer, andthis
will have a different value than it would if it were called via anIInterface2
pointer. In either case,this
will hold a valid pointer of typeIUnknown*
because of point #1, so if you simply assign*ppv = this
, the caller will be happy, from a C++ point of view. You'll have stored a value of typeIUnknown*
into a variable of that same type (see point #2), so everything's fine.However, COM has stronger rules than ordinary C++. In particular, it requires that any request for the
IUnknown
interface of an object must return the same pointer, no matter which "view" of that object was used to invoke the query. Therefore, it's not sufficient for your object to always assign merethis
into*ppv
. Sometimes callers would get theIInterface1
version, and sometimes they'd get theIInterface2
version. A proper COM implementation needs to make sure it returns consistent results. It will commonly have anif
-else
ladder checking for all supported interfaces, but one of the conditions will check for two interfaces instead of just one, the second beingIUnknown
:It doesn't matter which interface the
IUnknown
check is grouped with as long as the grouping doesn't change while the object still exists, but you'd really have to go out of your way to make that happen.