c++ - and - java upcasting y downcasting
¿Por qué exactamente necesito un upcast explícito al implementar QueryInterface() en un objeto con múltiples interfaces() (2)
El problema es que *ppv
suele ser un void*
- asignándole 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é rama se esté hablando.
Por lo tanto, debe lanzar this
puntero para asegurarse de que el compilador 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 compilador espera en el slot 0 de la tabla) de repente estarás llamando a0
!
Cuando se llama a c0
en una D
lo que hace el compilador 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.
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<IInterface1>( 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?
Está haciendo programació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.
- Tanto
IInterface1
comoIInterface2
descienden deIUnknown
, y supongamos que ninguno es un descendiente del otro. - Cuando algo llama a
QueryInterface(IID_IUnknown, (void**)&intf)
en su objeto,intf
se declarará como tipoIUnknown*
. - 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 para la interfaz IUnknown
de un objeto debe devolver el mismo puntero, sin importar qué "vista" de ese objeto se utilizó para invocar la consulta. Por lo tanto, no es suficiente para su objeto asignar 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 if
- else
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<IInterface1*>(this);
} else if (iid == IID_IInterface2) {
*ppv = static_cast<IInterface2*>(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.