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

时间:2009-11-16 15:20:44

标签: c++ visual-c++ com multiple-inheritance

假设我有一个实现两个或更多COM接口的类:

class CMyClass : public IInterface1, public IInterface2 {
};

我看到的几乎所有文档都表明,当我为IUnknown实现QueryInterface()时,我明确地将指向其中一个接口的指针:

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 ++将传递这个与我对Interface2的QI()略有不同,因为C ++每次都会使这个指向子对象?

2 个答案:

答案 0 :(得分:26)

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

这不是单继承的问题,因为对于单继承,所有类的基指针总是相同的(因为vtable只是为派生类扩展)。

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

所以你需要转换*ppv指针以确保编译器将正确的基指针(对于正确的vtable)放入class A { virtual void fa0(); virtual void fa1(); int a0; }; class B : public A { virtual void fb0(); virtual void fb1(); int b0; };

以下是单继承的示例:

[0] fa0
[1] fa1

表格A:

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

B表格:

B

请注意,如果您拥有A vtable并且将其视为A vtable,那么它只会起作用 - A成员的偏移正是您所期望的。< / p>

以下是使用多重继承的示例(使用上面的Bclass C { virtual void fc0(); virtual void fc1(); int c0; }; class D : public B, public C { virtual void fd0(); virtual void fd1(); int d0; }; 的定义)(注意:只是一个示例 - 实现可能会有所不同):

[0] fc0
[1] fc1

表格C:

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

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

Vtable for D:

D

[0] @A vtable [1] a0 [2] b0 [3] @C vtable [4] c0 [5] d0 的实际内存布局:

D

请注意,如果您将A vtable视为D它将起作用(这是巧合 - 您不能依赖它)。但是 - 如果您在调用C(编译器在vtable的插槽0中预期)时将c0 vtable视为a0,您将突然调用c0

当你在D上调用this时,编译器会做什么,它实际上传递了一个假的C指针,它有一个vtable,它看起来应该是C的方式}。

因此,当您在D上调用D函数时,需要调整vtable以指向@C对象的中间位置(位于{{1}} vtable)之前调用函数。

答案 1 :(得分:7)

您正在进行COM编程,因此在查看为什么QueryInterface以其实现方式实现之前,您需要回忆一些关于代码的事情。

  1. IInterface1IInterface2都来自IUnknown,我们假设两者都不是另一个的后代。
  2. 当某件事在您的对象上调用QueryInterface(IID_IUnknown, (void**)&intf)时,intf将被声明为IUnknown*类型。
  3. 您的对象有多个“视图” - 界面指针 - 而QueryInterface可以通过其中任何一个调用。
  4. 由于第3点,this定义中QueryInterface的值可能会有所不同。通过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检查与哪个界面组合在一起并不重要,但是你真的必须尽力去做发生。