为什么将 COM 指针参数强制转换为 void 而不是 IUnknown?

Why are COM pointer arguments cast as void instead of IUnknown?

抱歉,这是一个愚蠢的问题,但我不清楚为什么 COM 指针参数通常被转换为 (void**) 而不是 (IUnknown**)。然后有时实际上使用 IUnknown 指针,例如 IObjectWithSite::SetSite。谁能解释一下?

我猜你是在谈论通过指针 "return" 值的输出参数,例如:

IUnknown *u;
QueryInterface(IID_IUnknown, (void **)&u);

IDispatch *d;
QueryInterface(IID_IDispatch, (void **)&d);

注意: 如果您不熟悉使用指针传递到 "return" 值的概念。


QueryInterface的原型是:

HRESULT QueryInterface(REFIID iid, void **ptr);

参数 &u 的类型已经是 IUnknown ** (因此没有理由将其转换为它已经存在的类型,正如您似乎在暗示的那样)。强制转换是将类型更改为函数参数的预期类型。

在标准 C++ 中,上面的代码实际上是未定义的行为(严格的别名违规——您不能假装指针指向与实际不同类型的对象)。该函数的正确用法是:

void *temp;
QueryInterface(IID_IFoo, &temp);
IFoo *foo = static_cast<IFoo *>(temp);

不过,Microsoft 编译器支持带有 C 风格转换的版本。它对内存位置进行相同的更改,就好像接口指针已声明为 void * 而不是其实际类型。


为什么 QueryInterfacevoid ** 而不是 IUnknown **?好吧,如果您特别请求 IID_IUnknown,那么避免强制转换会很好,但是任何其他接口无论如何都需要 C 风格的强制转换。这可能会引起混淆,因为(对于其他接口)returned 指针不一定是有效的 IUnknown * 值。

在 C++ 编程中,您可以(也许应该)使用模板包装器 类 来执行所有正确的类型操作。 Windows API 调用与 C 兼容,因此它们不能包含强类型泛型。

注意。所有对 QueryInterface 的调用都应检查 return 值,为了简洁起见,我在这里省略了它。

在 "get-type" 接口方法中(如 IObjectWithSite::QueryInterfaceIObjectWithSite::GetSiteIMoniker::BindToObject 等),因为它不会改变任何东西,所以你会无论如何都必须进行转换,除非你确实需要一个 IUnknown* 引用,但你已经有了它,因为......你正在使用它(IUknown* 引用始终是相同的指针每个 COM rules).

IObjectWithSite::SetSite是一个"set-type"方法,所以给大家一个IUnknown*参考更有意义。

在某些静态方法(如 CoCreateInstance or CoGetObject I think they could have put IUnknown** there instead of void** but then, they would have two different styles. And you wouldn't be able to use the old IID_PPV_ARGS 宏)中可能更具争议性,它非常实用,建议将其作为编码实践以避免类型转换错误。

我建议您从 Don Box 获得权威 "Essential COM" 的副本,并阅读第 60 页(至少 :-)。

重要提示

这与传统或方便无关。函数签名是 COM 基础知识的结果,以允许它工作。 要求 按原样输入。如果您不想通读这个答案,这里有重要的要点: C++ 中强制转换的道德等价物是对 COM 中 QueryInterface 的调用。唯一允许在 COM 中使用 C++ 强制转换的情况是在实现 COM 对象的 QueryInterface.

详情

COM 中期望 void*(相对于 IUnknown*)地址作为输出参数的函数签名可以 return any 接口类型。如果在假设的实现中将其更改为 IUnknown**,则使用 COM 要么不切实际,要么完全不可能。

让我们进行 2 个思想实验,从一个会使 COM 无法使用的实验开始:

让我们假设 CoCreateInstance 是 return 一个 IUnknown* 而不是通过 void* 请求的真实接口。在这种情况下,客户端必须立即在 returned IUnknown* 上调用 QueryInterface 以接收他们请求的接口指针 1。这不实用2.

这立即导致无法解决的实验。假设 QueryInterface return 编辑了一个 IUnknown*。要获得真正的接口指针,客户端需要调用 QueryInterface。但这只有 return 一个 IUnknown*!到那时,COM 的可消费接口表面已经折叠成一个单一的接口,IUnknown

每当 COM return 指向接口的指针时,它必须 return 指向最终类型的指针。唯一匹配所有接口指针的编程语言类型确实是 void*.3 这应该可以解释为什么输出参数需要输入 void** 而不是 IUnknown**.

对于 IObjectWithSite::SetSite,另一方面,IUnknown* 是一个 输入 。该接口仍然接受 any COM 接口,但它需要作为指向(身份可比)IUnknown 接口的指针传递。


1 COM 不强制执行特定的对象布局。相反,它将对接口指针的请求委托给各自的 QueryInterface 实现。

2 即使忽略立即需要在 IUnknown 上调用 Release() 以将增加的引用计数作为一部分考虑在内QI 电话。

3 来自The Component Object Model: "The only language requirement for COM is that code is generated in a language that can create structures of pointers and, either explicitly or implicitly, call functions through pointers." 故意没有要求,就是接口继承可以使用语言级别的结构来实现。即使需要IFoo来实现IUnknown,在像C这样的编程语言中IFoo*IUnknown*之间也没有关系。