使用隐式转换进行向上转型而不是使用QueryInterface()在多重继承中是否合法?

5
假设我有一个实现两个或多个COM接口的类(就像这里一样):
class CMyClass : public IInterface1, public IInterface2 { 
};

QueryInterface()必须为同一接口的每个请求返回相同的指针(需要显式向上转换以进行正确的指针调整):

if( iid == __uuidof( IUnknown ) ) { 
    *ppv = static_cast<IInterface1*>( this );
    //call Addref(), return S_OK 
} else if( iid == __uuidof( IInterface1 ) ) {
    *ppv = static_cast<IInterface1*>( this );
    //call Addref(), return S_OK 
} else if( iid == __uuidof( IInterface2 ) ) {
    *ppv = static_cast<IInterface2*>( this );
    //call Addref(), return S_OK 
} else {
    *ppv = 0;
    return E_NOINTERFACE;
}

现在对象中有两个IUnknown - 一个是IInterface1的基础,另一个是IInterface2的基础。而且它们位于不同的子对象中
假设我调用了IInterface2QueryInterface() - 返回的指针将与调用IUnknownQueryInterface()时返回的指针不同。到目前为止还好。然后,我可以将检索到的IInterface2*传递给任何接受IUnknown*的函数,由于C++的隐式转换,指针将被接受,但它不会是与QueryInterface()调用IUnknown*返回的指针相同。实际上,如果该函数在调用时立即调用QueryInterface()以获取IUnknown,它将检索到不同的指针。
从COM的角度来看,这合法吗?当我有一个指向多重继承对象的指针并允许隐式向上转换时,我应该如何处理情况?

实际上,我从未见过任何代码使用COM身份规则。话虽如此,另一个IUnknown*是不合法的 - 你必须选择一个从QueryInterface返回。就你自己内部而言,使用实现对象的COM对象的C++ - 如果你进行强制转换,那么你根本没有使用COM,因此你所做的任何事情都是合法的C++,但不是合法的COM。 - Chris Becke
@Chris Becke: 我猜实现一些类似缓存的功能需要身份验证。 - sharptooth
4个回答

3

COM没有关于接口标识的规则,只有对象标识的规则。QI的第一个规则是,如果IID_Unknown在两个接口指针上执行QI,则如果它们由相同的对象实现,则必须返回相同的指针。您的QI实现正确。

如果没有关于接口标识的保证,COM方法不能假设它传递的IUnknown指针与它调用该指针上的QI时检索到的指针相同。因此,如果需要证明对象标识,则需要单独执行QI。


2
看起来有一个小误解。接口IInterface1IInterface2是纯虚的,没有单独的QueryInterface()用于IInterface1IInterface2。只有一个声明,即类CMyClass将实现IInterface1IInterface2的所有方法(CMyClass的方法集合是IInterface1IInterface2的方法集合的并集)。

因此,在类CMyClass中实现一个QueryInterface()一个AddRef()一个Release()方法。在QueryInterface()中,将指向CMyClass实例的指针转换为static_cast<IUnknown *>

更新:嗨!我回答完问题就不得不离开。直到现在我才能看到所有其他答案,并可以增加一些内容。

好的。你说如果你将IInterface1强制类型转换为IUnknown,并将IInterface2强制类型转换为IUnknown,则会收到两个不同的指针。你是对的!但是这并不重要。如果你比较两个指针的内容(哪个地址有QueryInterface()AddRef()Release()),你会发现两个指针的内容相同。所以我也是对的!

有许多关于如何在纯C中实现COM的好例子。在这种情况下,您需要定义一个静态结构,其中包含指向虚拟函数的指针,例如QueryInterface()AddRef()Release(),并将这样一个结构的指针作为QueryInterface()的结果返回。这再次表明,对于COM来说,只有您返回的内容很重要,而不是指针(重要的是您返回哪个虚函数表)。

还有一个小备注。在一些评论中,您提到有可能有许多QueryInterface()AddRef()Release()方法的实现。在这里,我不同意。原因是接口是纯抽象类,如果您定义了一个实现某些接口的类,则没有典型的类继承。您只有一个实现所有接口函数的类。如果您在C++中这样做,编译器将创建并填充具有相应指针的静态虚函数表,用于QueryInterface()AddRef()Release()等函数的唯一实现,但所有虚函数表指向相同的函数。

为了减少vtable的数量,微软引入了__declspec(novtable)或ATL_NO_VTABLE,但这不是你问题的一部分。

是的,我知道。但有一个正式的问题。当我从IInterface2隐式向上转换为IUnknown时,我得到的指针与我从QI()得到的指针不同。整个问题都关于一件事:在COM方面,那个IUnknown是否“合法”? - sharptooth
这些方法有许多实现,即使除了一个实现版本外,所有其他版本都将调用转发到实现它的单个版本。 - Ben Voigt

2
正如Hans所指出的那样,您的QueryInterface实现是正确的。
始终使用QueryInterface是COM对象的用户的责任。原因正是为了防止您指出的情况:将IInterface1*或IInterface2*指针强制转换为IUnknown*指针会产生不同的物理值。
在C++中,实现者无法阻止用户做错事情。
它是否会导致应用程序失败取决于应用程序是否关心比较COM对象的身份。 MSDN:组件对象模型的规则

对象身份。要求 给定对象实例上任何接口的QueryInterface调用 对于特定接口IUnknown 必须始终返回相同的物理指针值。这使得 在任何两个接口上调用QueryInterface(IID_IUnknown, ...)并比较 结果以确定它们是否指向同一实例的对象(相同的COM对象标识)。

正如Oleg所指出的那样,对象身份的失败将具有相当有限的影响,因为COM接口成员的调用基本上不受影响 - 如果函数签名匹配,则每个虚拟表条目将指向相同的函数地址。
所有COM智能指针实现在转换为不同接口或当前接口可疑时都使用QueryInterface。它们的比较运算符自动在每个输入指针上使用QueryInterface(IID_IUnknown, ...),以获取可以直接比较的物理IUnknown*指针。如果您选择在整个应用程序中使用原始指针,则对象身份的失败将开始影响您的应用程序。
一种特殊情况是,在类没有任何菱形继承的情况下,失败不会显现出来。然而,在COM中,隐式转换始终是非法的,无论它是否导致应用程序崩溃。

我不同意。这句话谈论的是对象标识,而不是接口标识。 - Hans Passant
1
@sharptooth:在考虑 COM 时,请尽量避免认为 IInterface1 继承自 IUnknown 的思维方式。更精确的说法是:IInterface1 包含成员 AddRef、Release 和 QueryInterface,以及它自己的函数。 - rwong
@Hans:我承认可能存在某种混淆。这个问题的标题说“隐式转换用于向上转换而不是 QueryInterface”。在问题内部,@sharptooth 问是否允许发生这种情况是非法的。(我将编辑我的答案以澄清) - rwong
无论如何,规则规定在同一对象上对同一接口进行两次QI调用必须返回相同的指针。通过向上转型不会违反此要求 - QI仍将正常工作。 - sharptooth
@Hans:我删除了我的旧评论,因为我没有清楚地说明哪一方(QI 实现者还是 COM 用户)应该/不应该使用 C++ 强制转换。我再次编辑了我的答案,以表明 @sharptooth 的 QI 是正确的。 - rwong
显示剩余2条评论

0
如果您有 interface IInterface1 : IDispatchinterface IInterface2 : IDispatch,那么在 IInterface1IInterface2 上对 IUnknown 进行 QI 必须根据对象标识规则返回相同的指针。
但是...
IInterface1 上对 IDispatch 进行 QI 可以返回与在 IInterface2 上对 IDispatch 进行 QI 不同的实现。
因此答案(再次)是:取决于情况。对于向上转换为 IUnknown,结果可能为负;对于向上转换为其他任何内容,结果可能为正。

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