views:

335

answers:

2

Recently I came across some strange behaviour of my application. It has been developed mainly in C# but CLI/C++ was also used to achieve better performance. I was getting a System.NullReferenceException in a very simple method at the TimeSpan comparison:

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

It was obvious that the only reference used in this expression was implicit this (this._timestamp). I added an assert statement and it turned out that this is actually null. After short investigation I managed to prepared short program presenting this phenomenon. It is C++/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;
}

Does anybody know how can it be possible? Is it a bug in the compiler?

+8  A: 

In C++ (and presumably in C++/CLI) there's nothing preventing you from trying to call methods on a NULL pointer. In most implementations, a virtual method call will crash at the point of the call because the runtime won't be able to read the virtual method table. However, a non-virtual method call is just a function call with some parameters, one of which is the this pointer. If it's null, then that is what is passed to the function.

I believe the result of calling any member function on a NULL (or nullptr) pointer is officially "undefined behaviour".

Greg Hewgill
+2  A: 

Thank you Greg for your answer it happens the way you describe it. However, I am not pleased with this situation because it means that I have to place

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

at the beginning of every method. Only this would guarantee that my method won't appear at the top of stack-trace as a faulty piece of code without argument validation.

I have never come across (this == null) when I was writing in C#. Therefore, I decided to find out how is it different from C++/CLI. I created a sample application in C++/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();
        }
    };
}

And a small program in C# that uses the C++/CLI classes with the same Main method:

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

Then I disassembled them with Red Gate's .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 
}

The important parts are:

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

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

Where:

  • call - Calls the method indicated by the passed method descriptor.
  • callvirt - Calls a late-bound method on an object, pushing the return value onto the evaluation stack.

And now the final conclusion:

C# compiler in VS 2008 treats every method as if it was virtual, thus it it always safe to assume that (this != null). In C++/CLI every method is called as it should so it is necessary to pay attention to the non-virtual method calls.

proxon