This == null//Как это возможно?

Недавно я столкнулся с каким-то странным поведением моего приложения. Он был разработан в основном на С#, но CLI/С++ также использовался для достижения лучшей производительности. Я получал исключение System.NullReferenceException в очень простом методе при сравнении TimeSpan:

TimeSpan _timestamp;
void UpdateFrame(TimeSpan timestamp)
{
    if(TimeSpan::Equals(_timestamp, timestamp) == false) 

Было очевидно, что единственная ссылка, используемая в этом выражении, была неявной (this._timestamp). Я добавил утверждение assert, и оказалось, что это фактически null. После короткого расследования мне удалось подготовить короткую программу, представляющую это явление. Это С++/CLI.

using namespace System;
using namespace System::Reflection;

public class Unmanaged
{
public:
    int value;
};

public ref class Managed
{
public:
    int value;

    Unmanaged* GetUnmanaged()
    {
        SampleMethod();
        return new Unmanaged();
    }

    void SampleMethod()
    {
        System::Diagnostics::Debug::Assert(this != nullptr);
        this->value = 0;
    }
};

public ref class ManagedAccessor
{
public:
    property Managed^ m;
};

int main(array<System::String ^> ^args)
{
    ManagedAccessor^ ma = gcnew ManagedAccessor();
    // Confirm that ma->m == null
    System::Diagnostics::Debug::Assert(ma->m == nullptr);
    // Invoke method on the null reference
    delete ma->m->GetUnmanaged();
    return 0;
}

Кто-нибудь знает, как это возможно? Это ошибка в компиляторе?

Ответ 1

В С++ (и, предположительно, в С++/CLI) ничего не мешает вам пытаться вызвать методы на указателе NULL. В большинстве реализаций вызов виртуального метода разбивается в точке вызова, потому что среда выполнения не сможет прочитать таблицу виртуальных методов. Однако вызов не виртуального метода - это просто вызов функции с некоторыми параметрами, одним из которых является указатель this. Если оно равно null, то это то, что передается функции.

Я считаю, что результат вызова любой функции-члена в указателе NULL (или nullptr) официально "поведение undefined".

Ответ 2

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

if(this == nullptr) throw gcnew ArgumentException("this");

в начале каждого метода. Только это гарантирует, что мой метод не будет отображаться в верхней части стека-трассировки как неисправный фрагмент кода без проверки аргумента.

Я никогда не сталкивался (это == null), когда писал на С#. Поэтому я решил узнать, как он отличается от С++/CLI. Я создал образец приложения в С++/CLI:

namespace ThisEqualsNull{
    public ref class A
    {
    public:
        void SampleMethod()
        {
            System::Diagnostics::Debug::Assert(this != nullptr);
        }
    };

    public ref class Program{
    public:
        static void Main(array<System::String ^> ^args)
        {
            A^ a = nullptr;
            a->SampleMethod();
        }
    };
}

И небольшая программа на С#, которая использует классы С++/CLI с тем же основным методом:

class Program
{
    static void Main(string[] args)
    {
        A a = null;
        a.SampleMethod();
    }
}

Затем я разобрал их с Red Gate.NET Reflector:

C++/CLI
.method public hidebysig static void Main(string[] args) cil managed
{
    .maxstack 1
    .locals ( [0] class ThisEqualsNull.A a)
    L_0000: ldnull 
    L_0001: stloc.0 
    L_0002: ldnull 
    L_0003: stloc.0 
    L_0004: ldloc.0 
    L_0005: call instance void ThisEqualsNull.A::SampleMethod()
    L_000a: ret 
}


C#
.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 1
    .locals init ( [0] class [ThisEqualsNull]ThisEqualsNull.A a)
    L_0000: nop 
    L_0001: ldnull 
    L_0002: stloc.0 
    L_0003: ldloc.0 
    L_0004: callvirt instance void [ThisEqualsNull]ThisEqualsNull.A::SampleMethod()
    L_0009: nop 
    L_000a: ret 
}

Важными частями являются:

C++/CLI
L_0005: call instance void ThisEqualsNull.A::SampleMethod()

C#
L_0004: callvirt instance void [ThisEqualsNull]ThisEqualsNull.A::SampleMethod()

Где:

  • call - вызов метода, указанного дескриптором переданного метода.
  • callvirt - вызов метода поздней привязки объекта, нажатие возвращаемого значения на стек оценки.

И теперь окончательный вывод:

Компилятор С# в VS 2008 рассматривает каждый метод, как если бы он был виртуальным, поэтому всегда можно предположить, что (this!= null). В С++/CLI каждый метод вызывается так, как следует, поэтому необходимо обратить внимание на вызовы не виртуального метода.