usando - ¿Cómo puede C#permitir métodos genéricos virtuales donde C++ no puede permitir métodos de plantillas virtuales?
no se pueden inferir a partir del uso c# (5)
C ++ no es compatible con los métodos de plantillas virtuales. La razón es que esto alteraría el vtable
cada vez que se vtable
una nueva instanciación de dicho método (debe agregarse a la vtable
).
Java, por el contrario, permite métodos genéricos virtuales. Aquí, también está claro cómo se puede implementar esto: los genéricos de Java se borran en tiempo de ejecución, por lo que un método genérico es un método habitual en tiempo de ejecución, por lo que no es necesario modificar la vtable
.
Pero ahora a C #. C # tiene genéricos reificados. Con genéricos reificados y especialmente cuando se usan tipos de valor como parámetros de tipo, tiene que haber diferentes versiones de un método genérico. Pero luego tenemos el mismo problema que tiene C ++: Tendríamos que modificar el vtable cada vez que se realizara una nueva instanciación de un método genérico.
No soy demasiado profundo en el funcionamiento interno de C #, por lo que mi intuición podría estar totalmente equivocada. Entonces, ¿alguien con un conocimiento más profundo sobre C # /. NET me dice cómo pueden implementar métodos virtuales genéricos en C #?
Aquí está el código para mostrar lo que quiero decir:
[MethodImpl(MethodImplOptions.NoInlining)]
static void Test_GenericVCall()
{
var b = GetA();
b.M<string>();
b.M<int>();
}
[MethodImpl(MethodImplOptions.NoInlining)]
static A GetA()
{
return new B();
}
class A
{
public virtual void M<T>()
{
}
}
class B : A
{
public override void M<T>()
{
base.M<T>();
Console.WriteLine(typeof(T).Name);
}
}
¿Cómo despacha el CLR al código JIT correcto cuando se llama a M
en la función Test_GenericVCall
?
C ++ generalmente compila directamente en código nativo, y el código nativo para C.Foo<int>(int)
y C.Foo<long>(long)
puede ser diferente. Además, C ++ generalmente almacena punteros al código nativo en el vtable. Combine estos, y verá que si C.Foo<T>
es virtual, entonces un puntero a cada creación de instancias necesita ser parte de la vtable.
C # no tiene ese problema. C # compila a IL, e IL está JITted a código nativo. Los vtables de IL no contienen punteros al código nativo, contienen punteros a IL (más o menos). Además de eso, los genéricos .NET no permiten especializaciones. Entonces, en el nivel IL, C.Foo<int>(int)
y C.Foo<long>(long)
siempre se verán exactamente iguales.
Por lo tanto, el problema C ++ simplemente no existe para C # y no es un problema que deba resolverse.
PD: El enfoque de Java también lo usa el tiempo de ejecución de .NET. A menudo, los métodos genéricos darán como resultado el mismo código nativo exacto , independientemente del argumento de tipo genérico, y en ese caso, solo habrá una instancia de ese método. Esta es la razón por la que a veces ve referencias al System.__Canon
en los rastros de pila y tal, ¿es un equivalente en tiempo de ejecución aproximado de Java ?
.
Ejecutar este código y analizar el IL y el ASM generado nos permite ver lo que está sucediendo:
internal class Program
{
[MethodImpl(MethodImplOptions.NoInlining)]
private static void Test()
{
var b = GetA();
b.GenericVirtual<string>();
b.GenericVirtual<int>();
b.GenericVirtual<StringBuilder>();
b.GenericVirtual<int>();
b.GenericVirtual<StringBuilder>();
b.GenericVirtual<string>();
b.NormalVirtual();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static A GetA()
{
return new B();
}
private class A
{
public virtual void GenericVirtual<T>()
{
}
public virtual void NormalVirtual()
{
}
}
private class B : A
{
public override void GenericVirtual<T>()
{
base.GenericVirtual<T>();
Console.WriteLine("Generic virtual: {0}", typeof(T).Name);
}
public override void NormalVirtual()
{
base.NormalVirtual();
Console.WriteLine("Normal virtual");
}
}
public static void Main(string[] args)
{
Test();
Console.ReadLine();
Test();
}
}
Me breakpoint Program.Test con WinDbg:
.loadby sos clr; ! bpmd CSharpNewTest CSharpNewTest.Program.Test
Luego Sosex.dll el excelente comando !muf
para mostrarme el origen intercalado, IL y ASM:
0:000> !muf
CSharpNewTest.Program.Test(): void
b:A
002e0080 55 push ebp
002e0081 8bec mov ebp,esp
002e0083 56 push esi
var b = GetA();
IL_0000: call CSharpNewTest.Program::GetA()
IL_0005: stloc.0 (b)
>>>>>>>>002e0084 ff15c0371800 call dword ptr ds:[1837C0h]
002e008a 8bf0 mov esi,eax
b.GenericVirtual<string>();
IL_0006: ldloc.0 (b)
IL_0007: callvirt A::GenericVirtuallong
002e008c 6800391800 push 183900h
002e0091 8bce mov ecx,esi
002e0093 ba50381800 mov edx,183850h
002e0098 e877e49b71 call clr!JIT_VirtualFunctionPointer (71c9e514)
002e009d 8bce mov ecx,esi
002e009f ffd0 call eax
b.GenericVirtual<int>();
IL_000c: ldloc.0 (b)
IL_000d: callvirt A::GenericVirtuallong
002e00a1 6830391800 push 183930h
002e00a6 8bce mov ecx,esi
002e00a8 ba50381800 mov edx,183850h
002e00ad e862e49b71 call clr!JIT_VirtualFunctionPointer (71c9e514)
002e00b2 8bce mov ecx,esi
002e00b4 ffd0 call eax
b.GenericVirtual<StringBuilder>();
IL_0012: ldloc.0 (b)
IL_0013: callvirt A::GenericVirtuallong
002e00b6 6870391800 push 183970h
002e00bb 8bce mov ecx,esi
002e00bd ba50381800 mov edx,183850h
002e00c2 e84de49b71 call clr!JIT_VirtualFunctionPointer (71c9e514)
002e00c7 8bce mov ecx,esi
002e00c9 ffd0 call eax
b.GenericVirtual<int>();
IL_0018: ldloc.0 (b)
IL_0019: callvirt A::GenericVirtuallong
002e00cb 6830391800 push 183930h
002e00d0 8bce mov ecx,esi
002e00d2 ba50381800 mov edx,183850h
002e00d7 e838e49b71 call clr!JIT_VirtualFunctionPointer (71c9e514)
002e00dc 8bce mov ecx,esi
002e00de ffd0 call eax
b.GenericVirtual<StringBuilder>();
IL_001e: ldloc.0 (b)
IL_001f: callvirt A::GenericVirtuallong
002e00e0 6870391800 push 183970h
002e00e5 8bce mov ecx,esi
002e00e7 ba50381800 mov edx,183850h
002e00ec e823e49b71 call clr!JIT_VirtualFunctionPointer (71c9e514)
002e00f1 8bce mov ecx,esi
002e00f3 ffd0 call eax
b.GenericVirtual<string>();
IL_0024: ldloc.0 (b)
IL_0025: callvirt A::GenericVirtuallong
002e00f5 6800391800 push 183900h
002e00fa 8bce mov ecx,esi
002e00fc ba50381800 mov edx,183850h
002e0101 e80ee49b71 call clr!JIT_VirtualFunctionPointer (71c9e514)
002e0106 8bce mov ecx,esi
002e0108 ffd0 call eax
b.NormalVirtual();
IL_002a: ldloc.0 (b)
002e010a 8bce mov ecx,esi
002e010c 8b01 mov eax,dword ptr [ecx]
002e010e 8b4028 mov eax,dword ptr [eax+28h]
IL_002b: callvirt A::NormalVirtual()
002e0111 ff5014 call dword ptr [eax+14h]
}
IL_0030: ret
De interés es la llamada virtual normal, que se puede comparar con las llamadas virtuales genéricas:
b.NormalVirtual();
IL_002a: ldloc.0 (b)
002e010a 8bce mov ecx,esi
002e010c 8b01 mov eax,dword ptr [ecx]
002e010e 8b4028 mov eax,dword ptr [eax+28h]
IL_002b: callvirt A::NormalVirtual()
002e0111 ff5014 call dword ptr [eax+14h]
Se ve muy estándar. Echemos un vistazo a las llamadas genéricas:
b.GenericVirtual<string>();
IL_0024: ldloc.0 (b)
IL_0025: callvirt A::GenericVirtuallong
002e00f5 6800391800 push 183900h
002e00fa 8bce mov ecx,esi
002e00fc ba50381800 mov edx,183850h
002e0101 e80ee49b71 call clr!JIT_VirtualFunctionPointer (71c9e514)
002e0106 8bce mov ecx,esi
002e0108 ffd0 call eax
Ok, entonces las llamadas virtuales genéricas se manejan cargando nuestro objeto b
(que está en esi
, moviéndose a ecx
), y luego llamando a clr!JIT_VirtualFunctionPointer
. Dos constantes también son empujadas: 183850
en edx
. Podemos concluir que este es probablemente el manejador de la función A.GenericVirtual<T>
, ya que no cambia para ninguno de los 6 sitios de llamadas. La otra constante, 183900
, parece ser el manejador de tipo para el argumento genérico. De hecho, SSCLI confirma las sospechas:
HCIMPL3(CORINFO_MethodPtr, JIT_VirtualFunctionPointer, Object * objectUNSAFE, CORINFO_CLASS_HANDLE classHnd, CORINFO_METHOD_HANDLE methodHnd)
Por lo tanto, la búsqueda se delega básicamente en JIT_VirtualFunctionPointer
, que debe preparar una dirección a la que se pueda llamar. Supuestamente, o JIT y devolver un puntero al código JIT, o hacer un trampolín que, cuando se llama por primera vez, JIT la función.
0:000> uf clr!JIT_VirtualFunctionPointer
clr!JIT_VirtualFunctionPointer:
71c9e514 55 push ebp
71c9e515 8bec mov ebp,esp
71c9e517 83e4f8 and esp,0FFFFFFF8h
71c9e51a 83ec0c sub esp,0Ch
71c9e51d 53 push ebx
71c9e51e 56 push esi
71c9e51f 8bf2 mov esi,edx
71c9e521 8bd1 mov edx,ecx
71c9e523 57 push edi
71c9e524 89542414 mov dword ptr [esp+14h],edx
71c9e528 8b7d08 mov edi,dword ptr [ebp+8]
71c9e52b 85d2 test edx,edx
71c9e52d 745c je clr!JIT_VirtualFunctionPointer+0x70 (71c9e58b)
clr!JIT_VirtualFunctionPointer+0x1b:
71c9e52f 8b12 mov edx,dword ptr [edx]
71c9e531 89542410 mov dword ptr [esp+10h],edx
71c9e535 8bce mov ecx,esi
71c9e537 c1c105 rol ecx,5
71c9e53a 8bdf mov ebx,edi
71c9e53c 03ca add ecx,edx
71c9e53e c1cb05 ror ebx,5
71c9e541 03d9 add ebx,ecx
71c9e543 a180832872 mov eax,dword ptr [clr!g_pJitGenericHandleCache (72288380)]
71c9e548 8b4810 mov ecx,dword ptr [eax+10h]
71c9e54b 33d2 xor edx,edx
71c9e54d 8bc3 mov eax,ebx
71c9e54f f77104 div eax,dword ptr [ecx+4]
71c9e552 8b01 mov eax,dword ptr [ecx]
71c9e554 8b0490 mov eax,dword ptr [eax+edx*4]
71c9e557 85c0 test eax,eax
71c9e559 7430 je clr!JIT_VirtualFunctionPointer+0x70 (71c9e58b)
clr!JIT_VirtualFunctionPointer+0x47:
71c9e55b 8b4c2410 mov ecx,dword ptr [esp+10h]
clr!JIT_VirtualFunctionPointer+0x50:
71c9e55f 395804 cmp dword ptr [eax+4],ebx
71c9e562 7521 jne clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)
clr!JIT_VirtualFunctionPointer+0x55:
71c9e564 39480c cmp dword ptr [eax+0Ch],ecx
71c9e567 751c jne clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)
clr!JIT_VirtualFunctionPointer+0x5a:
71c9e569 397010 cmp dword ptr [eax+10h],esi
71c9e56c 7517 jne clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)
clr!JIT_VirtualFunctionPointer+0x5f:
71c9e56e 397814 cmp dword ptr [eax+14h],edi
71c9e571 7512 jne clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)
clr!JIT_VirtualFunctionPointer+0x64:
71c9e573 f6401801 test byte ptr [eax+18h],1
71c9e577 740c je clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)
clr!JIT_VirtualFunctionPointer+0x85:
71c9e579 8b4008 mov eax,dword ptr [eax+8]
71c9e57c 5f pop edi
71c9e57d 5e pop esi
71c9e57e 5b pop ebx
71c9e57f 8be5 mov esp,ebp
71c9e581 5d pop ebp
71c9e582 c20400 ret 4
clr!JIT_VirtualFunctionPointer+0x6a:
71c9e585 8b00 mov eax,dword ptr [eax]
71c9e587 85c0 test eax,eax
71c9e589 75d4 jne clr!JIT_VirtualFunctionPointer+0x50 (71c9e55f)
clr!JIT_VirtualFunctionPointer+0x70:
71c9e58b 8b4c2414 mov ecx,dword ptr [esp+14h]
71c9e58f 57 push edi
71c9e590 8bd6 mov edx,esi
71c9e592 e8c4800400 call clr!JIT_VirtualFunctionPointer_Framed (71ce665b)
71c9e597 5f pop edi
71c9e598 5e pop esi
71c9e599 5b pop ebx
71c9e59a 8be5 mov esp,ebp
71c9e59c 5d pop ebp
71c9e59d c20400 ret 4
La implementación se puede ver en SSCLI, y parece que todavía es aplicable:
HCIMPL3(CORINFO_MethodPtr, JIT_VirtualFunctionPointer, Object * objectUNSAFE,
CORINFO_CLASS_HANDLE classHnd,
CORINFO_METHOD_HANDLE methodHnd)
{
CONTRACTL {
SO_TOLERANT;
THROWS;
DISABLED(GC_TRIGGERS); // currently disabled because of FORBIDGC in HCIMPL
} CONTRACTL_END;
OBJECTREF objRef = ObjectToOBJECTREF(objectUNSAFE);
if (objRef != NULL && g_pJitGenericHandleCache)
{
JitGenericHandleCacheKey key(objRef->GetMethodTable(), classHnd, methodHnd);
HashDatum res;
if (g_pJitGenericHandleCache->GetValueSpeculative(&key,&res))
return (CORINFO_GENERIC_HANDLE)res;
}
// Tailcall to the slow helper
ENDFORBIDGC();
return HCCALL3(JIT_VirtualFunctionPointer_Framed, OBJECTREFToObject(objRef), classHnd, methodHnd);
}
HCIMPLEND
Básicamente, verifica un caché para ver si hemos visto antes esta combinación de tipo / clase y, de lo contrario, lo envía a JIT_VirtualFunctionPointer_Framed
que llama a MethodDesc::GetMultiCallableAddrOfVirtualizedCode
para obtener una dirección del mismo. La llamada a MethodDesc
pasa la referencia de objeto y el manejador de tipo genérico para que pueda buscar a qué función virtual enviar y qué versión de la función virtual (es decir, con qué parámetro genérico).
Todo esto se puede ver en SSCLI si desea profundizar más: parece que esto no ha cambiado con la versión 4.0 del CLR.
En resumen, el CLR hace lo que cabría esperar; generar diferentes sitios de llamadas que transmiten información del tipo con el que se llama a la función virtual genérica. Esto luego se pasa al CLR para hacer el despacho. La complejidad es que CLR tiene que hacer un seguimiento de la función virtual genérica y de las versiones que tiene JIT''ted.
Ha pasado mucho tiempo desde que hice C # cosas, antes de C # genéricos, así que no sé cómo las implementaciones de C # generalmente hacen cosas internamente.
Sin embargo, en el lado de C ++, las plantillas virtuales están limitadas por el objetivo del diseño de traducir cada unidad de traducción de forma aislada.
El siguiente es un ejemplo hipotético de plantilla de función virtual, que no se compilará con C ++ actual:
#include <iostream>
using namespace std;
struct Base
{
template< int n >
virtual void foo() { cout << "Base::foo<" << n << ">" << endl; }
static auto instance() -> Base&;
};
auto main()
-> int
{
Base::instance().foo<666>();
}
//-------------------------------- Elsewhere:
struct Derived: Base
{
template< int n >
virtual void foo() { cout << "Derived::foo<" << n << ">" << endl; }
};
auto Base::instance() -> Base&
{
static Derived o;
return o;
}
Así es como se podría implementar manualmente:
#include <iostream>
#include <map>
#include <typeindex>
using namespace std;
struct Base
{
virtual ~Base() {}
template< int n >
struct foo_pointer
{
void (*p)( Base* );
};
template< int n >
using Foo_pointer_map = map<type_index, foo_pointer< n >>;
template< int n >
static
auto foo_pointer_map()
-> Foo_pointer_map< n >&
{
static Foo_pointer_map< n > the_map;
return the_map;
}
template< int n >
static
void foo_impl( Base* ) { cout << "Base::foo<" << n << ">" << endl; }
template< int n >
void foo() { foo_pointer_map< n >()[type_index( typeid( *this ) )].p( this ); }
static auto instance() -> Base&;
};
bool const init_Base = []() -> bool
{
Base::foo_pointer_map<666>()[type_index( typeid( Base ) )].p = &Base::foo_impl<666>;
return true;
}();
auto main()
-> int
{
Base::instance().foo<666>();
}
//-------------------------------- Elsewhere:
struct Derived: Base
{
template< int n >
static
void foo_impl( Base* ) { cout << "Derived::foo<" << n << ">" << endl; }
};
bool const init_Derived = []() -> bool
{
// Here one must know about the instantiation of the base class function with n=666.
Base::foo_pointer_map<666>()[type_index( typeid( Derived ) )].p = &Derived::foo_impl<666>;
return true;
}();
auto Base::instance() -> Base&
{
static Derived o;
return o;
}
Este código compila y arroja el resultado que uno esperaría del primer código, pero solo al usar el conocimiento sobre todas las instancias de la plantilla , las instancias que podrían estar en diferentes unidades de traducción.
En el punto donde se inicializan las tablas de búsqueda, este conocimiento generalmente no está disponible.
Aún así, los compiladores modernos de C ++ proporcionan una optimización de todo el programa con la posible generación de código en tiempo de enlace, por lo que probablemente no esté más allá de la tecnología actual. Es decir, no es una imposibilidad técnica, sino más bien una impracticabilidad. Además de eso, existe el problema de las bibliotecas dinámicas, que C ++, por supuesto, no es compatible, pero sigue siendo parte de la realidad práctica de la programación en C ++.
Llamaré a la template
C ++ sy al código de patrón de genéricos de C # para tener un término común.
Código de patrón en el punto donde genera las necesidades concretas del código:
- una descripción completa del patrón (el código fuente del patrón, o algo similar)
- información sobre los argumentos de patrón en los que se está creando una instancia
- un entorno de compilación lo suficientemente robusto como para combinar los dos
En C ++, el patrón genera código concreto en el nivel de la unidad de compilación. Tenemos el compilador completo, el código fuente completo de la template
y la información de tipo completo del argumento de la template
, así que agitamos y horneamos.
Los genéricos tradicionales (no reificados) también generan código concreto en un lugar similar, pero luego permiten la extensión del tiempo de ejecución con nuevos tipos. Por lo tanto, se utiliza el borrado del tipo de tiempo de ejecución en lugar de la información de tipo completo del tipo en cuestión. Aparentemente, Java solo hace esto para evitar la necesidad de un nuevo bytecode para los genéricos (ver la codificación anterior).
Los genéricos reificados empaquetan el código genérico sin procesar en algún tipo de representación lo suficientemente fuerte como para volver a aplicar el genérico en un nuevo tipo. En tiempo de ejecución, C # tiene una copia completa del compilador, y el tipo agregado también lleva básicamente información completa acerca de qué se compiló. Con las 3 partes, puede volver a aplicar el patrón en un nuevo tipo.
C ++ no tiene un compilador, no almacena suficiente información sobre tipos o plantillas para aplicar en tiempo de ejecución. Se han realizado algunos intentos para retrasar la instanciación de plantillas hasta el tiempo de enlace en C ++.
Entonces su método genérico virtual termina compilando un nuevo método cuando se pasa un nuevo tipo. En tiempo de ejecución.
Tanto las plantillas C ++ como los genéricos C # son características diseñadas para implementar el paradigma de programación genérica: escribir algoritmos y estructuras de datos que no dependen del tipo de datos que manipulan.
Pero trabajan de maneras muy diferentes .
Los genéricos trabajan inyectando información de tipo en el código para que sea aplicable en tiempo de ejecución. Entonces los diferentes algoritmos / estructuras de datos saben qué tipos están usando, adaptándose ellos mismos. Dado que la información de tipo es viable / accesible en tiempo de ejecución, ese tipo de decisiones se pueden hacer en tiempo de ejecución y dependen de la entrada del tiempo de ejecución . Es por eso que el polimorfismo (una decisión de tiempo de ejecución también) y los genéricos C # funcionan bien juntos.
Las plantillas de C ++, por otro lado, son una bestia muy diferente. Son un sistema de generación de código en tiempo de compilación. Eso significa que lo que hace el sistema de plantillas es generar en tiempo de compilación diferentes versiones del código según los tipos utilizados. Incluso si esto pudiera lograr muchas cosas poderosas que los genéricos no (de hecho, el sistema de plantillas de C ++ es Turing completo), la generación de código se realiza en tiempo de compilación, por lo que debemos conocer los tipos utilizados en tiempo de compilación .
Dado que las plantillas solo generan diferentes versiones del código para los diferentes tipos utilizados, dada una plantilla de template<typename T> void foo( const T& t );
función template<typename T> void foo( const T& t );
, foo( 1 )
y foo( ''c'' )
no llaman a la misma función , llaman a las versiones generadas int
y char
respectivamente.
Es por eso que el polimorfismo no se puede usar junto con plantillas: cada ejemplo de plantilla de función es una función distinta, por lo que hacer que la plantilla sea polimórfica no tiene sentido. ¿Qué versión debería llamar en tiempo de ejecución ?.