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

sha*_*oth 14 c++ com multiple-inheritance visual-c++

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

class CMyClass : public IInterface1, public IInterface2 {
};
Run Code Online (Sandbox Code Playgroud)

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

if( iid == __uuidof( IUnknown ) ) {
     *ppv = static_cast<IInterface1>( this );
     //call Addref(), return S_OK
}
Run Code Online (Sandbox Code Playgroud)

问题是为什么我不能复制这个

if( iid == __uuidof( IUnknown ) ) {
     *ppv = this;
     //call Addref(), return S_OK
}
Run Code Online (Sandbox Code Playgroud)

文档通常说如果我执行后者,我将违反对同一对象上的QueryInterface()的任何调用必须返回完全相同的值的要求.

我不太明白.难道他们的意思是,如果我QI()用于IInterface2和调用QueryInterface()通过该指针C++将通过略有不同,如果我QI()用于IInterface2因为C++每次都会使点到子对象?

Aar*_*ron 27

问题是*ppv通常是void*- 直接分配this它将简单地取现有this指针并给出*ppv它的值(因为所有指针都可以转换为void*).

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

但是 - 对于多重继承,您实际上最终会得到多个基本指针,具体取决于您正在讨论的类的"视图"!这样做的原因是,通过多重继承,您不能只扩展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;
};
Run Code Online (Sandbox Code Playgroud)

V的表格:

[0] fa0
[1] fa1
Run Code Online (Sandbox Code Playgroud)

B的vtable:

[0] fa0
[1] fa1
[2] fb0
[3] fb1
Run Code Online (Sandbox Code Playgroud)

请注意,如果您有Bvtable并且将其视为Avtable它只是起作用 - 成员的偏移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;
};
Run Code Online (Sandbox Code Playgroud)

适用于C的vtable:

[0] fc0
[1] fc1
Run Code Online (Sandbox Code Playgroud)

D的vtable:

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

@C:
[0] fc0
[1] fc1
[2] fd0
[3] fd1
Run Code Online (Sandbox Code Playgroud)

以及实际的内存布局D:

[0] @A vtable
[1] a0
[2] b0
[3] @C vtable
[4] c0
[5] d0
Run Code Online (Sandbox Code Playgroud)

请注意,如果您将Dvtable视为A可行(这是巧合 - 您不能依赖它).但是 - 如果你把Dvtable视为C你调用的时候c0(编译器在vtable的插槽0中所期望的那样)你会突然调用a0!

当你调用编译器所做c0D事情时,它实际上传递了一个假this指针,它有一个vtable,它看起来应该像a一样C.

因此,当您调用C函数时D,需要在调用函数之前调整vtable以指向D对象的中间(在@Cvtable处).


Rob*_*edy 8

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

  1. 双方IInterface1IInterface2从下降IUnknown,并假设既不是其他的后裔.
  2. 当某事件调用QueryInterface(IID_IUnknown, (void**)&intf)您的对象时,intf将声明为类型IUnknown*.
  3. 您的对象有多个"视图" - 接口指针 - QueryInterface可以通过其中任何一个调用.

因为第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;
Run Code Online (Sandbox Code Playgroud)

IUnknown只要在对象仍然存在的情况下分组没有改变,检查与哪个接口组合在一起并不重要,但是你真的必须尽力让这种情况发生.