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?
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".
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.