Serious bugs with lifted/nullable conversions from int, allowing conversion from decimal
I think this question will bring me instant fame here on Stack Overflow.
Suppose you have the following type:
// represents a decimal number with at most two decimal places after the period
struct NumberFixedPoint2
{
decimal number;
// an integer has no fractional part; can convert to this type
public static implicit operator NumberFixedPoint2(int integer)
{
return new NumberFixedPoint2 { number = integer };
}
// this type is a decimal number; can convert to System.Decimal
public static implicit operator decimal(NumberFixedPoint2 nfp2)
{
return nfp2.number;
}
/* will add more nice members later */
}
It has been written such that only safe conversions that don't lose precision are allowed. However, when I try this code:
static void Main()
{
decimal bad = 2.718281828m;
NumberFixedPoint2 badNfp2 = (NumberFixedPoint2)bad;
Console.WriteLine(badNfp2);
}
I am surprised this compiles and, when run, writes out 2
. The conversion from int
(of value 2
) to NumberFixedPoint2
is important here. (An overload of WriteLine
that takes in a System.Decimal
is preferred, in case anyone wonders.)
Why on Earth is the conversion from decimal
to NumberFixedPoint2
allowed? (By the way, in the above code, if NumberFixedPoint2
is changed from a struct to a class, nothing changes.)
Do you know if the C# Language Specification says that an implicit conversion from int
to a custom type "implies" the existence of a "direct" explicit conversion from decimal
to that custom type?
It becomes much worse. Try this code instead:
static void Main()
{
decimal? moreBad = 7.3890560989m;
NumberFixedPoint2? moreBadNfp2 = (NumberFixedPoint2?)moreBad;
Console.WriteLine(moreBadNfp2.Value);
}
As you see, we have (lifted) Nullable<>
conversions here. But oh yes, that does compile.
When compiled in x86 "platform", this code writes out an unpredictable numeric value. Which one varies from time to time. As an example, on one occasion I got 2289956
. Now, that's one serious bug!
When compiled for the x64 platform, the above code crashes the application with a System.InvalidProgramException
with message Common Language Runtime detected an invalid program. According to the documentation of the InvalidProgramException
class:
Generally this indicates a bug in the compiler that generated the program.
Does anyone (like Eric Lippert, or someone who has worked with lifted conversions in the C# compiler) know the cause of these bugs? Like, what is a sufficient condition that we don't run into them in our code? Because the type NumberFixedPoint2
is actually something that we have in real code (managing other people's money and stuff).
I'm just replying to the first part of the question to start with. (I suggest that the second part should be a separate question; it's more likely to be a bug.)
There's only an explicit conversion from decimal
to int
, but that conversion is being implicitly called in your code. The conversion happens in this IL:
IL_0010: stloc.0
IL_0011: ldloc.0
IL_0012: call int32 [mscorlib]System.Decimal::op_Explicit(valuetype [mscorlib]System.Decimal)
IL_0017: call valuetype NumberFixedPoint2 NumberFixedPoint2::op_Implicit(int32)
I believe this is the correct behaviour according to the spec, even though it's surprising1. Let's work our way through section 6.4.5 of the C# 4 spec (User-Defined Explicit Conversions). I'm not going to copy out all the text, as it would be tedious - just what the relevant results are in our case. Likewise I'm not going to use subscripts, as they don't work well with code font here :)
- Determine the types
S0
andT0
:S0
isdecimal
, andT0
isNumberFixedPoint2
. - Find the set of types,
D
, from which used-defined conversion operators will be considered: just{ decimal, NumberFixedPoint2 }
- Find the set of applicable user-defined and lifted conversion operators,
U
.decimal
encompassesint
(section 6.4.3) because there's a standard implicit conversion fromint
todecimal
. So the explicit conversion operator is inU
, and is indeed the only member ofU
- Find the most specific source type,
Sx
, of the operators inU
- The operator doesn't convert from
S
(decimal
) so the first bullet is out - The operator doesn't convert from a type that encompasses
S
(decimal
encompassesint
, not the other way round) so the second bullet is out - That just leaves the third bullet, which talks about the "most encompassing type" - well, we've only got one type, so that's okay:
Sx
isint
.
- The operator doesn't convert from
- Find the most specific target type,
Tx
, of the operators inU
- The operator convers straight to
NumberFixedPoint2
soTx
isNumberFixedPoint2
.
- The operator convers straight to
- Find the most specific conversion operator:
-
U
contains exactly one operator, which does indeed convert fromSx
toTx
, so that's the most specific operator
-
- Finally, apply the conversion:
-
If
S
is notSx
, then a standard explicit conversion fromS
toSx
is performed. (So that'sdecimal
toint
.) - The most specific user-defined conversion operator is invoked (your operator)
-
T
isTx
so there's no need for the conversion in the third bullet
-
If
The line in bold is the bit which confirms that a standard explicit conversion really is feasible, when only an explicit conversion from a different type is actually specified.
1 Well I found it surprising, at least. I'm not aware of seeing this before.
Your second portion (using nullable types) appears to be very similar to this known bug in the current compiler. From the response on the Connect issue:
While we do not currently have plans to address this issue in the next release of Visual Studio, we do plan to investigate a fix in Roslyn
As such, this bug will hopefully get corrected in a future release of Visual Studio and the compilers.