Подтвердить что ты не робот

Как С# разрешить виртуальные общие методы, где С++ не может допускать методы виртуального шаблона?

С++ не поддерживает методы виртуальных шаблонов. Причина в том, что это изменило бы значение vtable всякий раз, когда создается новый экземпляр такого метода (его нужно добавить в vtable).

Java в отличие от этого позволяет виртуальные общие методы. Здесь также ясно, как это можно реализовать: генерируемые Java файлы стираются во время выполнения, поэтому общий метод является обычным методом во время выполнения, поэтому никаких изменений в vtable не требуется.

Но теперь к С#. С# действительно обновил дженерики. При использовании редизайтных дженериков и особенно при использовании типов значений в качестве параметров типа должны быть разные версии общего метода. Но тогда у нас та же проблема, что и у С++: нам нужно было бы изменить vtable всякий раз, когда создавалось новое создание универсального метода.

Я не слишком глубоко во внутреннюю работу С#, поэтому моя интуиция может быть просто совершенно неправильной. Так может ли кто-нибудь с более глубокими знаниями о С#/.NET сказать мне, как они могут реализовать общие виртуальные методы в С#?

Здесь код, чтобы показать, что я имею в виду:

[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);
    }
}

Как CLR отправляет правильный код JIT при вызове M в функции Test_GenericVCall?

4b9b3361

Ответ 1

Запуск этого кода и анализ IL и сгенерированной ASM позволяет нам видеть, что происходит:

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();
    }
}

Я остановил программу. Тест с WinDbg:

.loadby sos clr;! bpmd CSharpNewTest CSharpNewTest.Program.Test

Затем я использовал Sosex.dll отличную команду !muf, чтобы показать мне перемеженный источник, IL и 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 

Представляет интерес обычный виртуальный вызов, который можно сравнить с общими виртуальными вызовами:

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]

Выглядит очень стандартно. Давайте рассмотрим общие вызовы:

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 (который находится в esi, перемещается в ecx), а затем вызывает в clr!JIT_VirtualFunctionPointer. Также переносятся две константы: 183850 в edx. Мы можем заключить, что это, вероятно, дескриптор функции A.GenericVirtual<T>, так как он не изменяется ни для одного из 6 сайтов вызовов. Другая константа 183900 выглядит как дескриптор типа для общего аргумента. Действительно, SSCLI подтверждает подозрения:

HCIMPL3(CORINFO_MethodPtr, JIT_VirtualFunctionPointer, Object * objectUNSAFE, CORINFO_CLASS_HANDLE classHnd, CORINFO_METHOD_HANDLE methodHnd)

Итак, поиск в основном делегируется JIT_VirtualFunctionPointer, который должен подготовить адрес, который можно вызвать. Предположительно, он либо JIT, либо возвращает указатель на код JIT, или создаст батут, который при вызове в первый раз будет JIT.

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

Реализация может быть просмотрена в SSCLI, и похоже, что она по-прежнему применима:

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

Итак, в основном он проверяет кеш, чтобы увидеть, видели ли мы эту комбинацию типа/класса раньше, и в противном случае отправляет его на JIT_VirtualFunctionPointer_Framed, который вызывает в MethodDesc::GetMultiCallableAddrOfVirtualizedCode, чтобы получить его адрес. Вызов MethodDesc передается ссылкой на объект и общий тип дескриптора, чтобы он мог искать, какую виртуальную функцию отправлять, и какую версию виртуальной функции (то есть с каким общим параметром).

Все это можно просмотреть в SSCLI, если вы хотите углубиться в суть - похоже, это не изменилось с версией CLR 4.0.

Короче говоря, CLR делает то, что вы ожидаете; генерировать разные сайты вызовов, которые несут информацию о типе, с которым вызвана виртуальная, общая функция. Затем он передается в CLR для отправки. Сложность заключается в том, что CLR должна как отслеживать общую виртуальную функцию, так и версии ее, что она имеет JIT.

Ответ 2

Я буду называть С++ template и С# generics 'pattern code', чтобы иметь общий термин.

Код шаблона в точке, где он генерирует конкретные потребности кода:

  • полное описание шаблона (исходный код шаблона или что-то подобное)
  • информация о параметрах-шаблонах, которые она создается на
  • среда компиляции достаточно прочная, чтобы объединить два

В С++ шаблон генерирует конкретный код на уровне единицы компиляции. У нас есть полный компилятор, весь исходный код template и полная информация о типе аргумента template, поэтому мы дрожаем и запекаем.

Традиционные дженерики (не переопределенные) также генерируют конкретный код в аналогичном месте, но затем позволяют расширению времени выполнения с новыми типами. Таким образом, стирание типа времени запуска используется вместо полной информации типа соответствующего типа. Очевидно, Java делает это только для того, чтобы избежать необходимости использования нового байт-кода для генериков (см. Выше кодировку).

Reified generics упаковывают исходный общий код в какое-то представление, которое достаточно сильное, чтобы повторно использовать общий тип нового типа. Во время выполнения С# имеет полную копию компилятора, а добавленный тип также содержит в себе полную информацию о том, из чего он был скомпилирован. Со всеми 3 частями он может повторно применить шаблон нового типа.

С++ не поддерживает компилятор, он не хранит достаточно информации о типах или шаблонах для применения во время выполнения. Были предприняты некоторые попытки отложить создание шаблона до времени ссылки на С++.

Таким образом, ваш виртуальный общий метод заканчивается компиляцией нового метода при передаче нового типа. Во время выполнения.

Ответ 3

Оба шаблона С++ и генераторы С# - это функции, предназначенные для реализации парадигмы обобщенного программирования: писать алгоритмы и структуры данных, которые не зависят от типа данных, которыми они управляют.

Но они работают по-разному..

Generics обрабатывает информацию типа инъекций в коде, который будет доступен во время выполнения. Поэтому различные алгоритмы/структуры данных знают, какие типы они используют, адаптируя себя. Так как информация о типе доступна/доступна во время выполнения, , такие титры могут быть выполнены во время выполнения и зависят от ввода времени выполнения. Вот почему полиморфизм (тоже время выполнения) и генераторы С# хорошо работают.

С++ шаблоны, с другой стороны, являются совсем другим зверем. Они представляют собой систему генерации кода времени компиляции. Это означает, что система шаблонов - это генерировать во время компиляции разные версии кода в зависимости от используемых типов. Даже если это может привести к множеству мощных вещей, которые не имеют дженериков (на самом деле система шаблонов С++ является Turing Complete), генерация кода выполняется во время компиляции, поэтому мы должны знать типы, используемые во время компиляции.
Поскольку шаблоны просто генерируют разные версии кода для разных используемых типов, с учетом шаблона функции template<typename T> void foo( const T& t );, foo( 1 ) и foo( 'c' ) не вызывать одну и ту же функцию, они вызывают int и char соответственно.

Вот почему полиморфизм нельзя использовать для шаблонов: каждый экземпляр шаблона функции имеет свою отличную функцию, поэтому создание полиморфного шаблона не имеет смысла. Какую версию следует вызывать во время выполнения?.

Ответ 4

С++ обычно компилируется прямо в собственный код, а собственный код для C.Foo<int>(int) и C.Foo<long>(long) может быть другим. Кроме того, С++ обычно сохраняет указатели на собственный код в таблице vtable. Объедините их, и вы увидите, что если C.Foo<T> является виртуальным, то указатель на каждый экземпляр должен быть частью vtable.

С# не имеет этой проблемы. С# компилируется в IL, а IL - в собственный код. IL vtables не содержат указателей на собственный код, они содержат указатели на IL (вид). Кроме того,.NET generics не разрешает специализацию. Таким образом, на уровне IL, C.Foo<int>(int) и C.Foo<long>(long) всегда будут выглядеть точно так же.

Следовательно, проблема С++ просто не существует для С# и не является проблемой, требующей решения.

P.S.: Java-подход также используется средой .NET. Часто общие методы приводят к тому же самому родному коду, независимо от аргумента generic type, и в этом случае будет только один экземпляр этого метода. Вот почему вы иногда видите ссылки на System.__Canon в трассировках стека и т.д., Это грубый эквивалент Java ? во время выполнения.

Ответ 5

Прошло много времени с тех пор, как я делал С#, прежде чем С# generics, поэтому я не знаю, как реализация С# обычно делает что-то внутренне.

Однако на стороне С++ виртуальные шаблоны ограничены целью дизайна перевода каждой единицы перевода .

Ниже приведен гипотетический пример шаблона виртуальной функции, который не будет компилироваться с текущим С++:

#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;
}

Вот как это можно реализовать вручную:

#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;
}

Этот код компилирует и дает результат, который можно было бы ожидать от первого кода, но только , используя знания обо всех экземплярах шаблона, экземпляры, которые могут быть в разных единицах перевода.

В тот момент, когда инициализируются таблицы поиска, это знание обычно недоступно.

Тем не менее, современные компиляторы С++ обеспечивают полную оптимизацию программы с возможным генерированием кода во время соединения, поэтому он, вероятно, не выходит за рамки текущей технологии. То есть не техническая невозможность, а скорее непрактичность. К этому добавилась проблема динамических библиотек, которые, конечно, С++ не поддерживают, но все же являются частью практической реальности программирования на С++.