¿Por qué exactamente necesito un upcast explícito al implementar QueryInterface () en un objeto con múltiples interfaces ()

Supongamos que tengo una clase implementando dos o más interfaces COM:

class CMyClass : public IInterface1, public IInterface2 { }; 

Casi todos los documentos que vi sugieren que cuando implemente QueryInterface () para IUnknown, explícitamente modifique este puntero a una de las interfaces:

 if( iid == __uuidof( IUnknown ) ) { *ppv = static_cast( this ); //call Addref(), return S_OK } 

La pregunta es por qué no puedo simplemente copiar esto ?

 if( iid == __uuidof( IUnknown ) ) { *ppv = this; //call Addref(), return S_OK } 

Los documentos generalmente dicen que si hago esto último, violaré el requisito de que cualquier llamada a QueryInterface () en el mismo objeto debe devolver exactamente el mismo valor.

No lo entiendo del todo. ¿Significan que si QI () para IInterface2 y call QueryInterface () a través de ese puntero C ++ pasará esto ligeramente diferente de si I QI () para IInterface2 porque C ++ cada vez hará que este punto sea un subobjeto?

El problema es que *ppv suele ser un void* : asignarle directamente this simplemente tomará el puntero existente y le dará *ppv el valor (ya que todos los punteros se pueden convertir a void* ).

Esto no es un problema con la herencia individual porque con la herencia única, el puntero base siempre es el mismo para todas las clases (porque el vtable se acaba de extender para las clases derivadas).

Sin embargo, para la herencia múltiple, en realidad terminas con múltiples punteros básicos, ¡dependiendo de la “vista” de la clase de la que estás hablando! La razón de esto es que con herencia múltiple no se puede simplemente extender el vtable; se necesitan múltiples vtables dependiendo de qué twig se esté hablando.

Por lo tanto, debe lanzar this puntero para asegurarse de que el comstackdor coloca el puntero base correcto (para la tabla de valores correcta) en *ppv .

Aquí hay un ejemplo de herencia simple:

 class A { virtual void fa0(); virtual void fa1(); int a0; }; class B : public A { virtual void fb0(); virtual void fb1(); int b0; }; 

vtable para A:

 [0] fa0 [1] fa1 

vtable para B:

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

Tenga en cuenta que si tiene B vtable y lo trata como un A vtable, simplemente funciona: las compensaciones para los miembros de A son exactamente lo que usted esperaría.

Aquí hay un ejemplo que usa herencia múltiple (usando las definiciones de A y B de arriba) (nota: solo un ejemplo – las implementaciones pueden variar):

 class C { virtual void fc0(); virtual void fc1(); int c0; }; class D : public B, public C { virtual void fd0(); virtual void fd1(); int d0; }; 

vtable para C:

 [0] fc0 [1] fc1 

vtable para D:

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

Y el diseño de memoria real para D :

 [0] @A vtable [1] a0 [2] b0 [3] @C vtable [4] c0 [5] d0 

Tenga en cuenta que si trata un D vtable como A , funcionará (esto es una coincidencia, no puede confiar en él). Sin embargo, si tratas un D vtable como C cuando llamas a c0 (que el comstackdor espera en el slot 0 de la tabla) de repente estarás llamando a0 !

Cuando se llama a c0 en un D lo que hace el comstackdor es que pasa un falso a this puntero que tiene un vtable que se ve como debería para una C

Por lo tanto, cuando llama a una función C en D , necesita ajustar el vtable para apuntar al centro del objeto D (en la @C ) antes de llamar a la función.

Está haciendo progtwigción COM, por lo que hay algunas cosas que recordar acerca de su código antes de ver por qué QueryInterface se implementa de la manera que es.

  1. Tanto IInterface1 como IInterface2 descienden de IUnknown , y supongamos que ninguno es un descendiente del otro.
  2. Cuando algo llama a QueryInterface(IID_IUnknown, (void**)&intf) en su objeto, intf se declarará como tipo IUnknown* .
  3. Hay múltiples “vistas” de su objeto, punteros de interfaz, y se podría llamar a QueryInterface través de cualquiera de ellos.

Debido al punto n. ° 3, el valor de this en su definición de QueryInterface puede variar. Llame a la función a través de un puntero IInterface1 , y this tendrá un valor diferente de lo que sería si se llamara a través de un puntero IInterface2 . En cualquier caso, this mantendrá un puntero válido de tipo IUnknown* debido al punto # 1, por lo que si simplemente asigna *ppv = this , la persona que llama estará contenta desde el punto de vista de C ++ . Habrás almacenado un valor de tipo IUnknown* en una variable del mismo tipo (ver el punto # 2), así que todo está bien.

Sin embargo, COM tiene reglas más fuertes que C ++ ordinario . En particular, requiere que cualquier solicitud de la interfaz IUnknown de un objeto devuelva el mismo puntero, sin importar qué “vista” de ese objeto se utilizó para invocar la consulta. Por lo tanto, no es suficiente que su objeto asigne siempre this a *ppv . A veces los llamantes IInterface1 versión IInterface1 y, a veces, IInterface2 versión IInterface2 . Una implementación COM adecuada debe asegurarse de que arroje resultados consistentes. Comúnmente tendrá una escalera ifelse compruebe todas las interfaces compatibles, pero una de las condiciones buscará dos interfaces en lugar de una sola, la segunda es IUnknown :

 if (iid == IID_IUnknown || iid == IID_IInterface1) { *ppv = static_cast(this); } else if (iid == IID_IInterface2) { *ppv = static_cast(this); } else { *ppv = NULL; return E_NOINTERFACE; } AddRef(); return S_OK; 

No importa con qué interfaz esté agrupada la verificación desconocida, siempre y cuando la agrupación no cambie mientras el objeto aún exista, pero realmente tendría que hacer todo lo posible para que eso suceda.