It looks like the issue is that when MS introduced nullable types, they made it so that every struct is implicitly convertable to its nullable type (foo?), so the code
if( f == null)
is equivalent to
if ( (Nullable<foo>)f == (Nullable<foo>)null)
Since MSDN states that "any user-defined operators that exist for value types may also be used by nullable types", when you override operator==, you allow that implicit cast to compile, as you now have a user-defined == -- giving you the nullable overload for free.
An aside:
Seems like in your example, there is some compiler optimization
The only thing that is emitted by the compiler that even hints there was a test is this IL:
ldc.i4.0
ldc.i4.0
ceq
stloc.1 //where there is an unused boolean local
Note that if you change main to
Foo f = new Foo();
object b = null;
if (f == b) { Console.WriteLine("?"); }
It no longer compiles. But if you box the struct:
Foo f = new Foo();
object b = null;
if ((object)f == b) { Console.WriteLine("?"); }
if compiles, emits IL, and runs as expected (the struct is never null);