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

     2023-03-29     179

关键词:

【中文标题】为啥在具有多个接口() 的对象中实现 QueryInterface() 时我需要显式向上转换【英文标题】:Why exactly do I need an explicit upcast when implementing QueryInterface() in an object with multiple interfaces()为什么在具有多个接口() 的对象中实现 QueryInterface() 时我需要显式向上转换 【发布时间】:2010-12-17 02:28:53 【问题描述】:

假设我有一个实现两个或多个 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 指向一个子对象?

【问题讨论】:

void** 在 QueryInterface() 示例中像往常一样。 也是如此:C* c = static_cast(this);保证指向正确子类的指针?还是我必须使用 dynamic_cast? static_cast 如果您转换为基类就足够了 - 编译器有足够的数据在编译期间进行所有必要的调整。编译器将拒绝对派生类执行 static_cast,但 dynamic_cast 仍然可以成功。 根据你的建议*ppv = this 将指向基地?我在标准中找不到任何可以证实这一点的东西。可以吗? *ppv = static_cast&lt;IInterface1&gt;( this ); 甚至这个语句也会导致未定义的行为,因为当类具有非标准布局时,标准不保证指针不可转换(即您将指针返回到 IInterface1,而不是 @987654327 @,只是假设它会是相同的)。而CMyClass 是一个非标准的布局类,原因有很多。在 10 年过去之后,我很高兴您能分享您目前对此事的了解。 【参考方案1】:

问题在于*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;
;

A 的 vtable:

[0] fa0
[1] fa1

用于 B 的 vtable:

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

请注意,如果您有 B 虚拟表,并且您将其视为 A 虚拟表,它就可以正常工作 - 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(编译器期望在 vtable 的插槽 0 中)时将 D vtable 视为 C,您将突然调用 a0

当您在D 上调用c0 时,编译器实际上会传递一个虚假的this 指针,该指针具有一个看起来与C 相同的vtable。

因此,当您在D 上调用C 函数时,它需要在调用该函数之前将vtable 调整为指向D 对象的中间(在@C vtable)。

【讨论】:

“取决于哪个视图”是否归结为“使用什么类型的指针调用QueryInterface()”? 基本上是的——调用者期望返回一个对象,该对象具有与他们期望的完全一样的 vtable 布局。然而 - 因为他们只是传递一个 void** 没有编译器强制类型安全(或隐式转换)。 很有趣,但是我想在 D 实例中实际上有 2 个 _vpointer,例如 @a0 A B @c0 C D,其中 A、B、C 和 D 实现了每个类的实际属性。如果您对涉及 MI 时的实际内存布局(对于给定的编译器)有更详细的解释,我将不胜感激。 @Matthiue M. - 正确 - 在 D 实例中会有 2 个 vtable。我稍微更新了描述以包含D 的示例内存布局。白板会更好... :)【参考方案2】:

您正在执行 COM 编程,因此在了解 QueryInterface 以这种方式实现的原因之前,需要回忆一些关于您的代码的事情。

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

由于第 3 点,this 的值在您的 QueryInterface 定义中可能会有所不同。通过IInterface1 指针调用函数,this 将具有与通过IInterface2 指针调用不同的值。在任何一种情况下,this 都会因为第 #1 点而持有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 检查与哪个接口分组无关紧要,只要在对象仍然存在时分组不会改变,但你真的必须竭尽全力才能做到这一点。

【讨论】:

我认为你错了。 This 将始终相同,因为所有方法都是虚拟的,纯虚拟 QueryInterface 的实际实现属于最派生类(从 COM 角度来看是对象)。 在基类的方法不是虚拟的并且基类有自己的实现并使用其this 的情况下可能会有所不同。请在上述场景中编译并检查this 地址,因为如果我是对的,那么您的答案就会产生误导。

在graphql中实现多个接口

...【问题描述】:我正在使用中继编译器,它不允许我编译具有实现多个接口的类型的模式。我做了一个小测试项目:package.json"scripts":"relay":"relay-compiler--src./--schemaschema.graphqls","depende 查看详情

为啥接口的泛型方法可以在 Java 中实现为非泛型?

】为啥接口的泛型方法可以在Java中实现为非泛型?【英文标题】:Whyagenericmethodofaninterfacecanbeimplementedasnon-genericinJava?为什么接口的泛型方法可以在Java中实现为非泛型?【发布时间】:2016-08-1002:49:11【问题描述】:假设我们有几... 查看详情

在 C++ 中实现多个接口

】在C++中实现多个接口【英文标题】:Implementingmultipleinterfacesinc++【发布时间】:2013-05-2010:07:44【问题描述】:我的接口层次结构如下:classApublic:voidfoo()=0;;classB:publicApublic:voidtestB()=0;;classC:publicApublic:voidtestC()=0;;现在,我想通过... 查看详情

在 QT 中实现具有多个小部件的视图的最佳方法是啥?

】在QT中实现具有多个小部件的视图的最佳方法是啥?【英文标题】:BestwaytoimplementaviewwithmultiplewidgetsinQT?在QT中实现具有多个小部件的视图的最佳方法是什么?【发布时间】:2011-02-2500:20:01【问题描述】:我想要一个视图,向用... 查看详情

在同步对象中实现异步接口

】在同步对象中实现异步接口【英文标题】:ImplementingAsynchronousInterfacesinSynchronousobjects【发布时间】:2018-11-0410:10:05【问题描述】:在学习异步编程时,我一直在尝试实现一个既适用于异步类又适用于同步类的接口,但我看到了... 查看详情

在 PHPUnit 中实现给定接口的模拟对象上的未定义方法?

】在PHPUnit中实现给定接口的模拟对象上的未定义方法?【英文标题】:UndefinedmethodonmockobjectimplementingagiveninterfaceinPHPUnit?【发布时间】:2012-09-1723:23:09【问题描述】:我是单元测试和PHPUnit的新手。我需要一个模拟,我可以完全控... 查看详情

如何在 Resilience4J 中实现多个具有相同配置的断路器

】如何在Resilience4J中实现多个具有相同配置的断路器【英文标题】:HowtoachievemultiplecircuitbreakerswithsameconfigurationinResilience4J【发布时间】:2020-08-2707:16:27【问题描述】:我是Resilience4J的新手,正在尝试与Springboot集成。我的应用程... 查看详情

从具有相同方法名称的多个接口继承

】从具有相同方法名称的多个接口继承【英文标题】:Inheritancefrommultipleinterfaceswiththesamemethodname【发布时间】:2011-01-2304:36:56【问题描述】:如果我们有一个继承自多个接口的类,并且这些接口有同名的方法,我们如何在我的类... 查看详情

在python中实现类接口的正确方法是啥

...:01【问题描述】:什么是让继承自超类的每个类在python中具有相同功能的正确方法?超类没有实现任何东西,它只是定义了子类所需的功能,供某些应用程序中的函数使用。在Python中实现这 查看详情

如何在 Java 中实现包装装饰器?

...Class中将所有原始方法调用委托给包装对象实现由另一个接口定义的所有方法添加到现有对象的接口是:publicinterfaceEnhancedNo 查看详情

用相同的方法在一个类中实现两个接口。哪个接口方法被覆盖?

...odisoverridden?【发布时间】:2011-02-1714:46:26【问题描述】:具有相同方法名称和签名的两个接口。但是由单个类实现,那么编译器将如何识别哪个方法用于哪个接口?例如:interfaceAintf();inte 查看详情

为啥我得到错误count():参数必须是在laravel中实现Countable的数组或对象?

】为啥我得到错误count():参数必须是在laravel中实现Countable的数组或对象?【英文标题】:whyimgettingerrorcount():ParametermustbeanarrayoranobjectthatimplementsCountableinlaravel?为什么我得到错误count():参数必须是在laravel中实现Countable的数... 查看详情

如何在基类中实现子类迭代器的统一接口?

...个基类Base,许多子类都派生自它。每个子类都包含一个具有某种类型和长度的数组。classBase//...intbaseData;virtualChildIte 查看详情

在 Java 中实现和使用通用的抽象接口

】在Java中实现和使用通用的抽象接口【英文标题】:Implementingandusingageneric,abstractinterfaceinJava【发布时间】:2021-12-1023:34:27【问题描述】:我的任务是创建一个谓词接口并实现一些实现该接口的类。例如,一个名为StartsWith的类是... 查看详情

多线程如何在c中实现?

...0c;是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片... 查看详情

多线程如何在c中实现?

...0c;是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片... 查看详情

使用消息传递接口在 Python 中实现多处理 [关闭]

...pt代码转换为Python,但是JavaScript以异步方式运行代码并且具有简单的事件发射器侦听器函数来在不同实例之间进行通信。在Pytho 查看详情

在 VBA 中实现接口的事件

】在VBA中实现接口的事件【英文标题】:Implementinginterface\'seventinVBA【发布时间】:2019-02-2713:19:00【问题描述】:我有一个界面IView:OptionExplicitPublicEventOnClientSelected()PublicPropertyGetClientNames()AsVariantEndProperty(...)但我无法在我的用户... 查看详情