为什么在具有多个接口的对象中实现 QueryInterface() 时需要显式向上转换()

发布于 2024-08-11 06:43:52 字数 739 浏览 1 评论 0原文

假设我有一个实现两个或多个 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

云柯 2024-08-18 06:43:52

问题是 *ppv 通常是 void* - 直接将 this 分配给它只会采用现有的 this > 指针并赋予 *ppv 它的值(因为所有指针都可以转换为 void*)。

这对于单继承来说不是问题,因为对于单继承,所有类的基指针始终相同(因为虚函数表只是针对派生类进行了扩展)。

然而,对于多重继承,您实际上最终会得到多个基指针,具体取决于您正在谈论的类的“视图”!这样做的原因是,通过多重继承,您不能只扩展 vtable - 您需要多个 vtable,具体取决于您正在讨论的分支。

因此,您需要转换 this 指针,以确保编译器将正确的基指针(对于正确的 vtable)放入 *ppv 中。

下面是单继承的示例:

class A {
  virtual void fa0();
  virtual void fa1();
  int a0;
};

class B : public A {
  virtual void fb0();
  virtual void fb1();
  int b0;
};

vtable for A:

[0] fa0
[1] fa1

vtable for B:

[0] fa0
[1] fa1
[2] fb0
[3] fb1

请注意,如果您有 B vtable 并且将其视为 A vtable,那么它就可以工作 - A 成员的偏移量正是您所期望的。

下面是一个使用多重继承的示例(使用上面的 AB 的定义)(注意:只是一个示例 - 实现可能会有所不同):

class C {
  virtual void fc0();
  virtual void fc1();
  int c0;
};

class D : public B, public C {
  virtual void fd0();
  virtual void fd1();
  int d0;
};

C 的 vtable:

[0] fc0
[1] fc1

D 的 vtable:

@A:
[0] fa0
[1] fa1
[2] fb0
[3] fb1
[4] fd0
[5] fd1

@C:
[0] fc0
[1] fc1
[2] fd0
[3] fd1

以及 D 的实际内存布局:

[0] @A vtable
[1] a0
[2] b0
[3] @C vtable
[4] c0
[5] d0

请注意,如果您将 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 a void* - directly assigning this to it will simply take the existing this pointer and give *ppv the value of it (since all pointers can be cast to void*).

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:

class A {
  virtual void fa0();
  virtual void fa1();
  int a0;
};

class B : public A {
  virtual void fb0();
  virtual void fb1();
  int b0;
};

vtable for A:

[0] fa0
[1] fa1

vtable for B:

[0] fa0
[1] fa1
[2] fb0
[3] fb1

Note that if you have the B vtable and you treat it like an A vtable it just works - the offsets for the members of A are exactly what you would expect.

Here's an example using multiple inheritance (using definitions of A and B from above) (note: just an example - implementations may vary):

class C {
  virtual void fc0();
  virtual void fc1();
  int c0;
};

class D : public B, public C {
  virtual void fd0();
  virtual void fd1();
  int d0;
};

vtable for C:

[0] fc0
[1] fc1

vtable for D:

@A:
[0] fa0
[1] fa1
[2] fb0
[3] fb1
[4] fd0
[5] fd1

@C:
[0] fc0
[1] fc1
[2] fd0
[3] fd1

And the actual memory layout for D:

[0] @A vtable
[1] a0
[2] b0
[3] @C vtable
[4] c0
[5] d0

Note that if you treat a D vtable as an A it will work (this is coincidence - you can't rely on it). However - if you treat a D vtable as a C when you call c0 (which the compiler expects in slot 0 of the vtable) you'll suddenly be calling a0!

When you call c0 on a D what the compiler does is it actually passes a fake this pointer which has a vtable which looks the way it should for a C.

So when you call a C function on D it needs to adjust the vtable to point to the middle of the D object (at the @C vtable) before calling the function.

我家小可爱 2024-08-18 06:43:52

您正在进行 COM 编程,因此在了解 QueryInterface 为何以这种方式实现之前,需要回忆一下有关代码的一些事项。

  1. IInterface1IInterface2 都是 IUnknown 的后代,我们假设两者都不是另一个的后代。
  2. 当某些对象在您的对象上调用 QueryInterface(IID_IUnknown, (void**)&intf) 时,intf 将被声明为类型 IUnknown*
  3. 您的对象有多个“视图”(接口指针),并且可以通过其中任何一个调用 QueryInterface。

由于第 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

if (iid == IID_IUnknown || iid == IID_IInterface1) {
  *ppv = static_cast<IInterface1*>(this);
} else if (iid == IID_IInterface2) {
  *ppv = static_cast<IInterface2*>(this);
} else {
  *ppv = NULL;
  return E_NOINTERFACE;
}
AddRef();
return S_OK;

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.

  1. Both IInterface1 and IInterface2 descend from IUnknown, and let's assume neither is a descendant of the other.
  2. When something calls QueryInterface(IID_IUnknown, (void**)&intf) on your object, intf will be declared as type IUnknown*.
  3. There are multiple "views" of your object — interface pointers — and QueryInterface could be called through any one of them.

Because point #3, the value of this in your QueryInterface definition can vary. Call the function via an IInterface1 pointer, and this will have a different value than it would if it were called via an IInterface2 pointer. In either case, this will hold a valid pointer of type IUnknown* 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 type IUnknown* 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 mere this into *ppv. Sometimes callers would get the IInterface1 version, and sometimes they'd get the IInterface2 version. A proper COM implementation needs to make sure it returns consistent results. It will commonly have an if-else ladder checking for all supported interfaces, but one of the conditions will check for two interfaces instead of just one, the second being IUnknown:

if (iid == IID_IUnknown || iid == IID_IInterface1) {
  *ppv = static_cast<IInterface1*>(this);
} else if (iid == IID_IInterface2) {
  *ppv = static_cast<IInterface2*>(this);
} else {
  *ppv = NULL;
  return E_NOINTERFACE;
}
AddRef();
return S_OK;

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文