Definition of "==" operator for Double

The source of the primitive types can be confusing. Have you seen the very first line of the Double struct?

Normally you cannot define a recursive struct like this:

public struct Double : IComparable, IFormattable, IConvertible
        , IComparable<Double>, IEquatable<Double>
{
    internal double m_value; // Self-recursion with endless loop?
    // ...
}

Primitive types have their native support in CIL as well. Normally they are not treated like object-oriented types. A double is just a 64-bit value if it is used as float64 in CIL. However, if it is handled as a usual .NET type, it contains an actual value and it contains methods like any other types.

So what you see here is the same situation for operators. Normally if you use the double type type directly, it will never be called. BTW, its source looks like this in CIL:

.method public hidebysig specialname static bool op_Equality(float64 left, float64 right) cil managed
{
    .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor()
    .custom instance void __DynamicallyInvokableAttribute::.ctor()
    .maxstack 8
    L_0000: ldarg.0
    L_0001: ldarg.1
    L_0002: ceq
    L_0004: ret
}

As you can see, there is no endless loop (the ceq instrument is used instead of calling the System.Double::op_Equality). So when a double is treated like an object, the operator method will be called, which will eventually handle it as the float64 primitive type on the CIL level.


I took a look at the CIL with JustDecompile. The inner == gets translated to the CIL ceq op code. In other words, it's primitive CLR equality.

I was curious to see if the C# compiler would reference ceq or the == operator when comparing two double values. In the trivial example I came up with (below), it used ceq.

This program:

void Main()
{
    double x = 1;
    double y = 2;

    if (x == y)
        Console.WriteLine("Something bad happened!");
    else
        Console.WriteLine("All is right with the world");
}

generates the following CIL (note the statement with label IL_0017):

IL_0000:  nop
IL_0001:  ldc.r8      00 00 00 00 00 00 F0 3F
IL_000A:  stloc.0     // x
IL_000B:  ldc.r8      00 00 00 00 00 00 00 40
IL_0014:  stloc.1     // y
IL_0015:  ldloc.0     // x
IL_0016:  ldloc.1     // y
IL_0017:  ceq
IL_0019:  stloc.2
IL_001A:  ldloc.2
IL_001B:  brfalse.s   IL_002A
IL_001D:  ldstr       "Something bad happened!"
IL_0022:  call        System.Console.WriteLine
IL_0027:  nop
IL_0028:  br.s        IL_0035
IL_002A:  ldstr       "All is right with the world"
IL_002F:  call        System.Console.WriteLine
IL_0034:  nop
IL_0035:  ret

The main confusion here is that you're assuming that all .NET libraries (in this case, the Extended Numerics Library, which is not a part of the BCL) are written in standard C#. This isn't always the case, and different languages have different rules.

In standard C#, the piece of code you're seeing would result in a stack overflow, due to the way operator overload resolution works. However, the code isn't actually in standard C# - it basically uses undocumented features of the C# compiler. Instead of calling the operator, it emits this code:

ldarg.0
ldarg.1
ceq
ret

That's it :) There is no 100% equivalent C# code - this simply isn't possible in C# with your own type.

Even then, the actual operator isn't used when compiling C# code - the compiler does a bunch of optimizations, like in this case, where it replaces the op_Equality call with just the simple ceq. Again, you can't replicate this in your own DoubleEx struct - it's compiler magic.

This certainly isn't a unique situation in .NET - there's plenty of code that isn't valid, standard C#. The reasons are usually (a) compiler hacks and (b) a different language, with the odd (c) runtime hacks (I'm looking at you, Nullable!).

Since the Roslyn C# compiler is oepn source, I can actually point you at the place where overload resolution is decided:

The place where all binary operators are resolved

The "shortcuts" for intrinsic operators

When you look at the shortcuts, you'll see that equality between double and double results in the intrinsic double operator, never in the actual == operator defined on the type. The .NET type system has to pretend that Double is a type like any other, but C# doesn't - double is a primitive in C#.


In reality, the compiler will turn the == operator into a ceq IL code, and the operator you mention will not be called.

The reason for the operator in the source code is likely so it can be called from languages other than C# that do not translate it into a CEQ call directly (or through reflection). The code within the operator will be compiled to a CEQ, so there is no infinite recursion.

In fact, if you call the operator via reflection, you can see that the operator is called (rather than a CEQ instruction), and obviously is not infinitely recursive (since the program terminates as expected):

double d1 = 1.1;
double d2 = 2.2;

MethodInfo mi = typeof(Double).GetMethod("op_Equality", BindingFlags.Static | BindingFlags.Public );

bool b = (bool)(mi.Invoke(null, new object[] {d1,d2}));

Resulting IL (compiled by LinqPad 4):

IL_0000:  nop         
IL_0001:  ldc.r8      9A 99 99 99 99 99 F1 3F 
IL_000A:  stloc.0     // d1
IL_000B:  ldc.r8      9A 99 99 99 99 99 01 40 
IL_0014:  stloc.1     // d2
IL_0015:  ldtoken     System.Double
IL_001A:  call        System.Type.GetTypeFromHandle
IL_001F:  ldstr       "op_Equality"
IL_0024:  ldc.i4.s    18 
IL_0026:  call        System.Type.GetMethod
IL_002B:  stloc.2     // mi
IL_002C:  ldloc.2     // mi
IL_002D:  ldnull      
IL_002E:  ldc.i4.2    
IL_002F:  newarr      System.Object
IL_0034:  stloc.s     04 // CS$0$0000
IL_0036:  ldloc.s     04 // CS$0$0000
IL_0038:  ldc.i4.0    
IL_0039:  ldloc.0     // d1
IL_003A:  box         System.Double
IL_003F:  stelem.ref  
IL_0040:  ldloc.s     04 // CS$0$0000
IL_0042:  ldc.i4.1    
IL_0043:  ldloc.1     // d2
IL_0044:  box         System.Double
IL_0049:  stelem.ref  
IL_004A:  ldloc.s     04 // CS$0$0000
IL_004C:  callvirt    System.Reflection.MethodBase.Invoke
IL_0051:  unbox.any   System.Boolean
IL_0056:  stloc.3     // b
IL_0057:  ret 

Interestingly - the same operators do NOT exist (either in the reference source or via reflection) for integral types, only Single, Double, Decimal, String, and DateTime, which disproves my theory that they exist to be called from other languages. Obviously you can equate two integers in other languages without these operators, so we're back to the question "why do they exist for double"?