127

For some reason I was sneaking into the .NET Framework source for the class Double and found out that the declaration of == is:

public static bool operator ==(Double left, Double right) {
    return left == right;
}

The same logic applies for every operator.


  • What's the point of such a definition?
  • How does it work?
  • Why doesn't it create an infinite recursion?
Thomas Ayoub
  • 29,063
  • 15
  • 95
  • 142
  • 18
    I´d expect an endless recursion. – MakePeaceGreatAgain Feb 01 '16 at 15:08
  • am i right, that this overrides the comparison with == only for two double values? So in the return you could add anoter specification – M. Schena Feb 01 '16 at 15:12
  • 6
    I am pretty sure it is not used for comparison anywhere with double, instead `ceq` is issued in IL. This is just there to fill some documentation purpose, Can't find the source though. – Habib Feb 01 '16 at 15:18
  • 2
    Most likely so that this operator can be obtained through Reflection. – Damien_The_Unbeliever Feb 01 '16 at 15:18
  • 3
    That will never be called, the compiler has the equality logic baked in (ceq opcode) see [When is Double's == operator invoked?](http://stackoverflow.com/questions/14958015/when-is-doubles-operator-invoked) – Alex K. Feb 01 '16 at 15:19
  • Another interesting puzzle are the Negative/Positive Infinity and NaN constants. They are defined as `(double)-1.0 / (double)(0.0);`, `(double)1.0 / (double)(0.0);` and `(double)0.0 / (double)0.0;` - I would expect a Divide By Zero exception there. – Zohar Peled Feb 01 '16 at 15:19
  • 1
    @ZoharPeled dividing a double with zero is valid and will result in positive or negative infinity. – Magnus Feb 01 '16 at 15:24
  • @Magnus thanks, I found that out when testing myself, but I still can't shake what all of my math teachers said about dividing by zero :-) – Zohar Peled Feb 01 '16 at 15:30
  • 1
    @ZoharPeled At levels of math before calculus and limits it's easier to teach that "you can't divide by zero" than to teach that "the limit of `x/y` as `y` approaches 0 is infinity" – D Stanley Feb 01 '16 at 15:48
  • @DStanley Thanks for jogging my memory... Now that you've mentioned it I do recall something about limits from high school, but that was such a long time ago :-) – Zohar Peled Feb 01 '16 at 15:52
  • 1
    The `System.Double` struct (and the `System.Single` struct) seem special. I couldn't find the relevant part of the C# language spec where this is explained though. Interestingly, the integral simple types (such as `Int32`, `Int64` etc) do *not* come with an overloaded equals-operator. Very curious to know why... – Dirk Vollmar Feb 01 '16 at 15:55
  • @DStanley That isn't true. The limit doesn't exist because depending on the direction you approach from it will be positive or negative infinity, so the left and right limit don't agree so the limit doesn't exist at that point. – Nils Ole Timm Feb 02 '16 at 13:21
  • @NilsOleTimm Fair enough - "as `y` approaches 0 _from the right_"... – D Stanley Feb 02 '16 at 20:39
  • @Thomas Your "correction" from "every operator" to "every operators" is wrong. I'd change it, but there's nothing else to fix in this post. – Nic Mar 03 '16 at 17:48
  • Did you research for yourself? – Dan Jul 06 '19 at 00:12
  • @Maverick you mean *by myself*? – Thomas Ayoub Jul 06 '19 at 16:22

5 Answers5

63

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

D Stanley
  • 149,601
  • 11
  • 178
  • 240
  • 13
    The only problem I can see with this is that the C# language specification says that overloaded operators take precedence over built-in operators. So surely, a conforming C# compiler should see that an overloaded operator is available here and generate the infinite recursion. Hmm. Troubling. – Damien_The_Unbeliever Feb 01 '16 at 15:26
  • 5
    That doesn't answer the question, imho. It only explains what the code is translated to but not why. According to section *7.3.4 Binary operator overload resolution* of the C# language specification I also would expect infinite recursion. I'd assume that the reference source (http://referencesource.microsoft.com/#mscorlib/system/double.cs,1a65cbdb09544ba1) doesn't really apply here. – Dirk Vollmar Feb 01 '16 at 15:30
  • @Damien_The_Unbeliever As stated, I suspect that the IL generated by the source code _does_ emit a `CEQ` operator and thus does not recurse infinitely. So either it's an exception to the spec for the `double` class (all value types, really), or the spec considers the operators directly defined on the value types as "built-in" operators. – D Stanley Feb 01 '16 at 15:33
  • 6
    @DStanley - I'm not denying what is produced. I'm saying I cannot reconcile it with the language spec. That's what's troubling. I was thinking about poring through Roslyn and seeing if I could find any special handling here but I'm not well set up to do this at present (wrong machine) – Damien_The_Unbeliever Feb 01 '16 at 15:34
  • 1
    @Damien_The_Unbeliever That's why I think it's either an exception to the spec or a different interpretation of "built-in" operators. – D Stanley Feb 01 '16 at 15:44
  • Interesting. The "user-defined" `operator ==` can only have the same signature as the ___predefined___ `operator ==` (setcion 7.10.2 in the C# spec) because the "user" here is the author of the `Double` structure. So this can only happen if the "user" is the author of the C# predefined types (see e.g. section 4.1.4). So that is somewhat special. Still a curious situation. The type member [seems to have been added in .NET 4.0](https://msdn.microsoft.com/en-us/library/system.double.op_equality.aspx). – Jeppe Stig Nielsen Feb 01 '16 at 16:05
  • `DateTime` which you now mention is not ___predefined___ in C#. So formally its CIL method `op_equality` should be called. Also, there is magic with the `Kind` of a `DateTime`, so we cannot simply compare as two `int64` values. There, `TimeSpan` is simpler (but also not ___predefined___ in C#). – Jeppe Stig Nielsen Feb 01 '16 at 16:24
  • @JeppeStigNielsen Which disproves my hypothesis that they exist to allow them to be called from other languages. Obviously you can equate two integers in other languages, so we're back to "why do they exist for `double`"? – D Stanley Feb 01 '16 at 16:56
  • Your hypothesis is actually standing fine, because the extended numerics library (which is where `Single`, `Double` and `Decimal` are defined) isn't part of the BCL. Both the BCL and ENL are a part of the C# specification, though, so any conforming C# implementation must also support floats and decimals (and they are primitives *in the context of C#*). – Luaan Feb 01 '16 at 17:01
  • And just for completion sake, both `DateTime` and `String` implement their own `==` operators, no cheats needed, so that also doesn't invalidate your hypothesis. `DateTime` is completely unimportant, since it's not a primitive, and `String`, well... strings are always tricky, no matter how much you pretend they're not :D – Luaan Feb 01 '16 at 17:10
  • 1
    As @Jon Skeet did not yet answer, or comment on, this, I suspect it is a bug (i.e. violation of spec). – TheBlastOne Feb 03 '16 at 06:19
37

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#.

Luaan
  • 62,244
  • 7
  • 97
  • 116
  • 1
    Not sure I agree that the code in the reference source is just "reverse engineered". The code has compiler directives (`#if`s) and other artifacts that would not be present in compiled code. Plus if it were reverse engineered for `double` then why wasn't it reverse engineered for `int` or `long`? I do think there's a reason for the source code but believe that the use of `==` inside the operator gets compiled to a `CEQ` which prevents recursion. Since the operator is a "predefined" operator for that type (and can't be overridden) the overload rules don't apply. – D Stanley Feb 02 '16 at 02:42
  • @DStanley I didn't want to imply that *all* the code is reverse engineered. And again, `double` is not part of the BCL - it's in a separate library, that just happens to be included in the C# specification. Yes, the `==` gets compiled to a `ceq`, but that still means this is a compiler hack you can't replicate in your own code, and something that isn't part of the C# specification (just like the `float64` field on the `Double` struct). It's not a contractual part of C#, so there's little point in treating it like valid C#, even if it was compiled with the C# compiler. – Luaan Feb 02 '16 at 08:55
  • @DStanely I couldn't find how the real framework is organized, but in the reference implementation of .NET 2.0, all of the tricky parts are just compiler intrinsics, implemented in C++. There's still lots of .NET native code, of course, but things like "comparing two doubles" wouldn't really work well in pure .NET; that's one of the reason floating point numbers aren't included in the BCL. That said, the code is *also* implemented in (non-standard) C#, probably exactly for the reason you mentioned earlier - to make sure other .NET compilers can treat those types as real .NET types. – Luaan Feb 02 '16 at 09:05
  • @DStanley But okay, point taken. I removed the "reverse engineered" reference, and reworded the answer to explicitly mention "standard C#", rather than just C#. And don't treat `double` the same way as `int` and `long` - `int` and `long` are primitive types that *all* the .NET languages must support. `float`, `decimal` and `double` aren't. – Luaan Feb 02 '16 at 09:14
12

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
György Kőszeg
  • 17,093
  • 6
  • 37
  • 65
  • 1
    For those who do not understand the first part of this post (maybe because they do not usually write their own value types), try the code `public struct MyNumber { internal MyNumber m_value; }`. It cannot be compiled, of course. The error is __error CS0523: Struct member 'MyNumber.m_value' of type 'MyNumber' causes a cycle in the struct layout__ – Jeppe Stig Nielsen Feb 01 '16 at 15:52
8

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
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Daniel Pratt
  • 12,007
  • 2
  • 44
  • 61
-2

As indicated in the Microsoft documentation for the System.Runtime.Versioning Namespace:The types found in this namespace are intended for use within the .NET Framework and not for user applications.The System.Runtime.Versioning namespace contains advanced types that support versioning in side by side implementations of the .NET Framework.