11

I have the following code:

public struct Num<T>
{
    private readonly T _Value;

    public Num(T value)
    {
        _Value = value;
    }

    static public explicit operator Num<T>(T value)
    {
        return new Num<T>(value);
    }
}

...
double d = 2.5;
Num<byte> b = (Num<byte>)d;

This code compiles, and it surprises my. The explicit convert should only accept a byte, not a double. But the double is accepted somehow. When I place a breakpoint inside the convert, I see that value is already a byte with value 2. By casting from double to byte should be explicit.

If I decompile my EXE with ILSpy, I see the next code:

double d = 2.5;
Program.Num<byte> b = (byte)d;

My question is: Where is that extra cast to byte coming from? Why is that extra cast place there? Where did my cast to Num<byte> go to?

EDIT
The struct Num<T> is the entire struct, so no more hidden extra methods or operators.

EDIT
The IL, as requested:

IL_0000: nop
IL_0001: ldc.r8 2.5 // Load the double 2.5.
IL_000a: stloc.0
IL_000b: ldloc.0
IL_000c: conv.u1 // Once again the explicit cast to byte.
IL_000d: call valuetype GeneriCalculator.Program/Num`1<!0> valuetype GeneriCalculator.Program/Num`1<uint8>::op_Explicit(!0) 
IL_0012: stloc.1
IL_0013: ret
Martin Mulder
  • 12,642
  • 3
  • 25
  • 54
  • 2
    You may want to consider looking at the raw IL rather than the C#. – Kirk Woll May 07 '13 at 18:20
  • @Kirk: IL added. Makes it even more confusing! :) – Martin Mulder May 07 '13 at 18:29
  • It appears to me that line `IL_000d` is doing exactly what you want and that the C# decompilation was in error. – Kirk Woll May 07 '13 at 18:34
  • I disagree. where does `conv.u1` come from? And since when is my operator translated into `op_Implicit` and not `op_Explicit`? – Martin Mulder May 07 '13 at 18:35
  • Does double inherit byte??? If so, I won't be surprised. – Daniel Möller May 07 '13 at 18:47
  • 1
    @Daniel Of course not. They're both value types, so they both only can inherit from `ValueType`. A `double` holds more information than a byte, so there isn't an implicit conversion to `byte`, it's a lossy operation. There's an *explicit* conversion, but it's not being explicitly called. – Servy May 07 '13 at 18:54
  • I can see how this happened. We can't do anything about it. You'll need to file this at connect.microsoft.com. The amount of engineering required to fix it would probably be substantial so making a case that you cannot find a workaround for it would be difficult. – Hans Passant May 07 '13 at 19:00
  • 3
    @HansPassant: There's nothing to fix here. The compiler is behaving correctly. (There are subtle bugs that the C# compiler team has "won't fix"d in user defined explicit conversions, but this isn't one of them.) – Eric Lippert May 07 '13 at 19:46
  • Hmm, connect is still a pretty good way to penetrate the Redmond Bubble. Surely the product you are working on is going to flag this statement? Or are you going to skip it because it is legal? – Hans Passant May 08 '13 at 13:00

3 Answers3

16

Let's take a step back and ask some clarifying questions:

Is this program legal?

public struct Num<T>
{
    private readonly T _Value;

    public Num(T value)
    {
        _Value = value;
    }

    static public explicit operator Num<T>(T value)
    {
        return new Num<T>(value);
    }
}

class Program
{
    static void Main()
    {
        double d = 2.5;
        Num<byte> b = (Num<byte>)d;
    }
}

Yes.

Can you explain why the cast is legal?

As Ken Kin pointed out, I explain that here:

Chained user-defined explicit conversions in C#

Briefly: a user-defined explicit conversion may have a built-in explicit conversion inserted on "both ends". That is, we can insert an explicit conversion either from the source expression to the parameter type of the user-defined conversion method, or from the return type of the user-defined conversion method to the target type of the conversion. (Or, in some rare cases, both.)

In this case we insert a built-in explicit conversion to the parameter type, byte, so your program is the same as if you'd written:

        Num<byte> b = (Num<byte>)(byte)d;

This is desirable behaviour. A double may be explicitly converted to byte, so a double may also be explicitly converted to Num<byte>.

For a complete explanation, read section 6.4.5 "User-defined explicit conversions" in the C# 4 specification.

Why does the IL generated call op_Implicit instead of op_Explicit?

It doesn't; the question is predicated on a falsehood. The above program generates:

  IL_0000:  nop
  IL_0001:  ldc.r8     2.5
  IL_000a:  stloc.0
  IL_000b:  ldloc.0
  IL_000c:  conv.u1
  IL_000d:  call       valuetype Num`1<!0> valuetype Num`1<uint8>::op_Explicit(!0)
  IL_0012:  stloc.1
  IL_0013:  ret

You're looking at an old version of your program probably. Do a clean rebuild.

Are there other situations in which the C# compiler silently inserts an explicit conversion?

Yes; in fact this is the second time that question has come up today. See

C# type conversion inconsistent?

Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • @Eric: About my `op_Explicit` vs `op_Implict` confusion: You are right, I corrected my question. – Martin Mulder May 08 '13 at 05:23
  • @Eric: Do I understand correctly when I say: When the compiler has to do an explicit conversion (because the code says so), it takes the freedom to use another explicit conversion to get the job done? – Martin Mulder May 08 '13 at 05:24
  • @MartinMulder: Correct. As I showed on my blog, you can actually get *four* explicit conversions for the price of one in some contrived cases. However you never get two *user-defined* conversions for the price of one. If Animal has a UDC to Fruit and Fruit has a UDC to Shape, you can cast Giraffe to Apple and Apple to Triangle, but you can't cast Giraffe directly to Triangle. – Eric Lippert May 08 '13 at 05:27
  • @Eric: Thanks. But... this story is not over yet... Today I will ask a second question about the same topic. I'll keep you in the loop. (BTW: Your first example code on your blog is not entirly correct. The operator misses a parameter of type Castle.) – Martin Mulder May 08 '13 at 05:31
  • @MartinMulder: As you might have seen some books, or even downloaded sample code, would left some subtle *missing* or *error*, it's alright. These subtle parts especially caught you eyes. – Ken Kin May 08 '13 at 10:10
  • @Eric Lippert: In your article you wrote "A user-defined conversion can have built-in conversions inserted automatically on the call and return sides, but we never automatically insert other user-defined conversions.". Is there are reason why system explicit conversions are treated differently than user explicit conversions? – Martin Mulder May 10 '13 at 09:55
  • 1
    @MartinMulder: Because then *those* user-defined conversions might need conversions on "either side" of them! And now we have to do yet another overload resolution problem, which might lead to yet *more* overload resolution problems. And what are the odds that the result that comes out the other end is the chain of conversions that the user intended? Pretty low, actually. The feature you're describing would be a bug farm, both for the compiler developers and for the developers using the feature. – Eric Lippert May 10 '13 at 14:34
10

First off, let's have a look at Mr. Lippert's blog:

Chained user-defined explicit conversions in C#

The compiler will sometimes1 insert the explicit conversion for us:

  • Part of the blogpost:

    ...

    When a user-defined explicit cast requires an explicit conversion on either the call side or the return side, the compiler will insert the explicit conversions as needed.

    The compiler figures that if the developer put the explicit cast in the code in the first place then the developer knew what they were doing and took the risk that any of the conversions might fail.

    ...

As this question, it's just one of the time of sometimes. What explicit conversion the compiler inserted is like we writing in the following code:

  • Test generic method with explicit conversion

    public static class NumHelper {
        public static Num<T> From<T>(T value) {
            return new Num<T>(value);
        }
    }
    
    public partial class TestClass {
        public static void TestGenericMethodWithExplicitConversion() {
            double d=2.5;
            Num<byte> b=NumHelper.From((byte)d);
        }
    }
    

    and the generated IL of the test method is:

    IL_0000: nop
    IL_0001: ldc.r8 2.5
    IL_000a: stloc.0
    IL_000b: ldloc.0
    IL_000c: conv.u1
    IL_000d: call valuetype Num`1<!!0> NumHelper::From<uint8>(!!0)
    IL_0012: stloc.1
    IL_0013: ret
    

Let's go one step back, to see the test of explicit operator as your question:

  • Test explicit operator

    public partial class TestClass {
        public static void TestExplicitOperator() {
            double d=2.5;
            Num<byte> b=(Num<byte>)d;
        }
    }
    

    and you've already seen the IL before:

    IL_0000: nop
    IL_0001: ldc.r8 2.5
    IL_000a: stloc.0
    IL_000b: ldloc.0
    IL_000c: conv.u1
    IL_000d: call valuetype Num`1<!0> valuetype Num`1<uint8>::op_Explicit(!0)
    IL_0012: stloc.1
    IL_0013: ret
    

Do you notice that they are quite similar? The difference is that the parameter !0 is a generic parameter in a type definition of your original code, and !!0 in the generic method test, is generic parameter in a method definition. You might want to have a look at chapter §II.7.1 of the specification Standard ECMA-335.

However, the most point here, is they both get into the type <uint8>(which is byte) of the generic definition; and as I mentioned above, according to Mr. Lippert's blogpost told us that the compiler sometimes inserts the explicit conversion when you did specify them explicitly!

Finally, as you suppose this is strange behavior of the compiler, and let me guess what is possibly you would think the compiler should do:

  • Test generic method by specifying type parameter:

    public partial class TestClass {
        public static void TestGenericMethodBySpecifyingTypeParameter() {
            double d=2.5;
            Num<byte> b=NumHelper.From<byte>(d);
        }
    }
    

Did I guess correctly? Anyway, what we are interested here, again, the IL. And I cannot wait to see the IL, it is:

0PC4l.png

Ooooops .. seem it's not as what the compiler thought an explicit operator would behaves like.

For conclution, when we specified the conversion explicitly, it's pretty semantic to say that we are expecting to convert one thing to another, the compiler deduced that and insert the obvious necessary convertion of the involved types; and once it found the type involved is not legal to be converted, it complains, just as we specified a more simply wrong conversion, such as (String)3.1415926 ...

Wish it's now more helpful without losing the correctness.

1: It's my personal expression of sometimes, in the blogpost actually is said as needed.


The following is some test for contrasting, when one possibly expect to convert the type with a existent explicit operator; and I put some comments in the code for describing each case:

double d=2.5;
Num<byte> b=(Num<byte>)d; // explicitly
byte x=(byte)d; // explicitly, as the case above

Num<byte> y=d; // no explicit, and won't compile

// d can be `IConvertible`, compiles
Num<IConvertible> c=(Num<IConvertible>)d;

// d can be `IConvertible`; 
// but the conversion operator is explicit, requires specified explicitly
Num<IConvertible> e=d;

// d cannot be `String`, won't compile even specified explicitly
Num<String> s=(Num<String>)d;

// as the case above, won't compile even specified explicitly
String t=(String)d; 

Maybe it's easier to understand.

Ken Kin
  • 4,503
  • 3
  • 38
  • 76
  • I don't understand your answer. Please elaborate. – Kirk Woll May 07 '13 at 18:25
  • How could `Num b=(Num)d` happen? A cast from `double` to `Num` does not exist. And I do not understand what you are trying to say with the third line of code. – Martin Mulder May 07 '13 at 18:25
  • @Ken: I see you edited your answer, but still... there is NO explicit conversion from `double` to `Num`. A explicit cast from double to byte DOES exist. So lines 2 should not compile, and yes, line 3 will compile. – Martin Mulder May 07 '13 at 18:40
  • To your edit, there is an *implicit* conversion from `double` to `IConvertible`, so that makes complete sense, but there is only an *explicit* conversion from `double` to `byte`, which is a very different matter. – Servy May 07 '13 at 18:56
  • 7
    I don't see why this answer has been voted down. It is correct. – Eric Lippert May 07 '13 at 19:36
  • @EricLippert Yes, it is correct, but it's not *helpful*. It just dumps some code and makes a few statements. Yes, those statements are correct, but it's not answering the question or explaining the behavior that the OP is not expecting, or why it behaves that way. Because of that, the answer is *correct*, but not *helpful*, thus a downvote. Oh, and also note the answer was much less helpful in it's previous revisions, which was when it attracted it's downvotes. It's somewhat more helpful now. – Servy May 07 '13 at 19:50
  • @Servy: I've once thought that put my explanation as the comment of code is more clear than moving those words farer from the code .. – Ken Kin May 07 '13 at 19:57
  • @KenKin I'm aware that you have code comments. It's basically just a list of whether or not the line will compile. Again, you're explaining what the code does (and even there not in depth at all) while the question is asking **why**. You aren't answering the **why**. – Servy May 07 '13 at 20:00
  • @KenKin From the question itself it's obvious that an explicit conversion from double to byte is being added by the compiler, despite the fact that it's not specified in code. *Why is the compiler implicitly adding this explicit conversion*? That's the question. The most your answer states is that it adds it. Well *duh*, that's what the OP figured out in the question. "*Why* does the compiler do this?" is the question. What's your answer to that? – Servy May 07 '13 at 20:11
  • The only information in this answer that I see as helpful is the link, which actually does explain *exactly* what the OP wants to know. The rest is all noise. It's stating information that's either irrelevant (even if it's correct) or is already in the OP. Had you simply summarized the linked article effectively you'd have a fine answer. Keep in mind that valuable content in an answer needs to be in the answer, not in a link. – Servy May 07 '13 at 20:13
  • @Servy: Ah .. maybe I should quote some explanation from Mr. Lippert's blog. Thank you very much. – Ken Kin May 07 '13 at 20:18
  • @KenKin I also think that your answer would be improved by removing most, if not all, of what you have in the code blocks. It's just showing a reproduction of what the OP is seeing, which isn't answering his questions, and adds noise that distracts from what you'll hopefully be adding to really answer the question. – Servy May 07 '13 at 20:22
  • @Servy I only downvote answers when they are completely wrong or incomprehensible. I upvote answers when they are correct and helpful. I don't vote on answers that are correct but not very helpful, or helpful but slightly wrong. Ken Kin's current answer is correct and helpful. – Daniel A.A. Pelsmaeker May 07 '13 at 21:13
  • @Ken Kin: As other people point out. Your first answer (just 3 lines of code and 1 line of english), was far away from being a helpful answer. It is still not very helpful. SO states that a helpful answer must stand on his own. Now you have a link to another page with an explaination and in your answer just some examples. But in your text nothing is realy explained. If the other pages would be removed, your answer falls apart. – Martin Mulder May 08 '13 at 05:37
  • @Virtlink You are certainly free to vote however you want; I don't have the right to say that you cannot vote using that metric; I was merely explaining the likely reasoning for others that had downvoted Ken Kin's post so that he would know how he could improve it. Also note that the tooltip for the voting buttons specifically states "this post is helpful" for upvote and "this post is not helpful" for downvotes, so that is the intended metric of the system. While correctness is related to helpfulness, not all correct posts are helpful, they just need to be correct in order to be helpful. – Servy May 08 '13 at 14:00
0

The relevant section of the C# standard (ECMA-334) is §13.4.4. I have emphasized in bold the parts relevant to your code above.

A user-defined explicit conversion from type S to type T is processed as follows:

[omitted]

  • Find the set of applicable conversion operators, U. This set consists of the user-defined and, if S and T are both nullable, lifted implicit or explicit conversion operators (§13.7.3) declared by the classes or structs in D that convert from a type encompassing or encompassed by S to a type encompassing or encompassed by T. If U is empty, there is no conversion, and a compile-time error occurs.

The terms encompassing and encompassed by are defined in §13.4.2.

Specifically, the conversion operator from byte to Num<byte> will be considered when casting double to Num<byte> because byte (the actual parameter type to the operator method) can be implicitly converted to double (i.e. byte is encompassed by the operand type double). User-defined operators like this are only considered for explicit conversions, even if the operator is marked implicit.

Sam Harwell
  • 97,721
  • 20
  • 209
  • 280
  • 1
    I've never liked this part of the spec; the whole notion of "encompassing" is messed up. For example: double encompasses byte, and therefore in this example we see that you can insert a built-in conversion from double to byte before the user-defined conversion. But double is neither encompassed by nor encompasses decimal, because neither is *implicitly* convertible to the other **even though both are explicitly convertible to the other**. Why is the "encompassing" relation relevant here? Surely the relevant relation is the "is explicitly convertible to" relation. The whole thing is a mess. – Eric Lippert May 07 '13 at 19:59
  • What is meant by `D`? It would seem it can't be an open-ended set, since the number of possible conversion operators would otherwise be limitless, but what is it exactly? – supercat May 07 '13 at 20:22
  • @EricLippert That would indicate the conversion `(Num)d`, (where `d` is a `double`) would not be valid. (My point being - while it may be strange or unintuitive, the semantics seem to be well-defined by this portion of the spec.) – Sam Harwell May 07 '13 at 20:34
  • 1
    @supercat: D is defined in the section of the spec not quoted here. If we have a conversion from S to T then D is S0, T0 and all base classes of S0 and T0. S0 and T0 are either S and T, or the their underlying types if they are nullable. So that set is finite. However your criticism does apply elsewhere; there are situations where the spec implies that overload resolution is choosing from an infinite set, which is plainly not the case. Mads is aware of them. – Eric Lippert May 07 '13 at 21:30
  • @280Z28: Correct; that conversion is not legal. But why should explicit double-to-`Num` be legal *because byte to double is implicit*? Surely the relevant decision point would be that it is legal *because double to byte is explicit*. This part of the spec is pretty well-defined, it's just a *bizarre* choice of predicate. – Eric Lippert May 07 '13 at 21:32
  • 1
    @EricLippert: It makes sense that the compiler wouldn't search in types derived *from* T, even conversion operators defined in those types might have a better match for S, since the compiler has no way of knowing identifying all the candidate types. It seems odd that the set would include base types of T, though, since that would seem to open the possibility of ambiguous conversions. If conversions exist from `Double` to a base type of `T`, and from `Byte` to `T`, which will be preferred if one tries to cast a `Double` to a `T`, or will the compiler refuse to do either? – supercat May 07 '13 at 22:03
  • @supercat: To answer your first question: like any overload resolution problem, all the candidates are put into a set and overload resolution attempts to find the unique best element of that set. It is slightly different than regular overload resolution in that regular overload resolution does not consider the return type, but the idea is the same. The exact details are in the spec. – Eric Lippert May 07 '13 at 22:28
  • @EricLippert: An off-topic thing: I actually asked http://stackoverflow.com/questions/14619574/how-to-implement-a-dealer-class-without-storing-a-deck-of-cards 3 months ago, without getting an applicable answer. I've just read up the whole series of http://ericlippert.com/2013/05/06/producing-permutations-part-seven/, you've already answered that question in the blog, and even more. Thank you very much! – Ken Kin May 08 '13 at 00:25
  • 3
    @KenKin: You're welcome! I've been wanting to write that series for a long time. – Eric Lippert May 08 '13 at 05:20