What's compiler thinking about the switch-statement?
Asked Answered
L

3

16

Inspired from a -5 question again!

I read [this comment] of @Quartermeister and been astonished!

So why this compiles

switch(1) {
    case 2:
}

but this doesn't.

int i;

switch(i=1) {
    case 2: // Control cannot fall through from one case label ('case 2:') to another
}

neither this

switch(2) {
    case 2: // Control cannot fall through from one case label ('case 2:') to another
}

update:

The -5 question became -3.

Lonna answered 7/3, 2013 at 20:32 Comment(8)
It's possible that the compiler can optimize out the first one, but cannot remove the second one, because of the assignment.Retinol
My guess is that 1 is a constant expression known at compile time, letting the compiler optimize out the entire switch, while i=1 is a non-constant expression (albeit also known to the compiler to produce a specific value) so the compiler tries to keep the switch.Junie
In an ideal world, the compiler should probably either accept or reject both of these. It certainly shouldn't generate any code for them. But in this world, I think it's just another example of "The Forrest Gump effect: "Stupid is as stupid does" :) Q: Why waste time/braincells even discussing it?Gaunt
how is it different from: int i=1; switch(i) { case 2: // Control cannot fall through from one case label ('case 1:') to another } which I believe is obvious...Moriah
so we won't need Eric Lippert to answer this. :)Buhr
I'm calling Eric Lippert by the way..Chantal
@evanmcdonnal: Yes, it is allowed. switch (i = 1){case 1: Console.WriteLine("foo"); break;} That compiles, runs, and outputs "foo".Intimist
Since you seem to be interested in the switch statement, you might want to read this: blogs.msdn.com/b/ericlippert/archive/2009/08/13/…Skeg
S
21

None of them should compile. The C# specification requires that a switch section have at least one statement. The parser should disallow it.

Let's ignore the fact that the parser allows an empty statement list; that's not what's relevant. The specification says that the end of the switch section must not have a reachable end point; that's the relevant bit.

In your last example, the switch section has a reachable end point:

void M(int x) { switch(2) { case 2: ; } }

so it must be an error.

If you had:

void M(int x) { switch(x) { case 2: ; } }

then the compiler does not know if x will ever be 2. It assumes conservatively that it could, and says that the section has a reachable end point, because the switch case label is reachable.

If you had

void M(int x) { switch(1) { case 2: ; } }

Then the compiler can reason that the endpoint is not reachable because the case label is not reachable. The compiler knows that the constant 1 is never equal to the constant 2.

If you had:

void M(int x) { switch(x = 1) { case 2: ; } }

or

void M(int x) { x = 1; switch(x) { case 2: ; } }

Then you know and I know that the end point is not reachable, but the compiler does not know that. The rule in the specification is that reachability is only determined by analyzing constant expressions. Any expression which contains a variable, even if you know its value by some other means, is not a constant expression.

In the past the C# compiler had bugs where this was not the case. You could say things like:

void M(int x) { switch(x * 0) { case 2: ; } }

and the compiler would reason that x * 0 had to be 0, therefore the case label is not reachable. That was a bug, which I fixed in C# 3.0. The specification says that only constants are used for that analysis, and x is a variable, not a constant.

Now, if the program is legal then the compiler can use advanced techniques like this to influence what code is generated. If you say something like:

void M(int x) { if (x * 0 == 0) Y(); }

Then the compiler can generate the code as though you'd written

void M(int x) { Y(); }

if it wants. But it cannot use the fact that x * 0 == 0 is true for the purposes of determining statement reachability.

Finally, if you have

void M(int x) { if (false) switch(x) { case 2: ; } }

then we know that the switch is not reachable, therefore the block does not have a reachable end point, so this is, surprisingly, legal. But given the discussion above, you now know that

void M(int x) { if (x * 0 != 0) switch(x) { case 2: ; } }

does not treat x * 0 != 0 as false, so the end point is considered reachable.

Skeg answered 7/3, 2013 at 21:43 Comment(2)
Thank you. I tested for switch(x) { }, it compiles, shouldn't it?Lonna
@KenKin: That's legal; a switch is permitted to contain zero switch sections.Skeg
I
2

In Visual Studio 2012, the reason for the first is obvious. The compiler determines that the code is unreachable:

switch (1)
{
    case 2:
}

Warning: Unreachable code detected.

In the other two cases, the compiler reports "Control cannot fall through from one case label ('case 2:') to another". I do not see it saying "('case 1')" in either of the failing cases.

I guess the compiler just isn't aggressive about constant evaluation. For example, the following are equivalent:

int i;
switch(i=1)
{
    case 2:
}

and

int i = 1;
switch(i)
{
    case 2:
}

In both cases, the compiler attempts to generate code, when it could do the evaluation and determine that what you're writing is:

switch (1)
{
    case 2:
}

And determine that the code is unreachable.

I suspect the "why doesn't this compile" answer will be "because we let the JIT compiler handle aggressive optimization."

Intimist answered 7/3, 2013 at 21:24 Comment(0)
R
1

Alright, so the problem with this is that the compiler completely optimizes away the switch, and here's proof:

static void withoutVar()
{
    Console.WriteLine("Before!");

    switch (1)
    {
        case 2:
    }

    Console.WriteLine("After!");
}

Which, when decompiled with ILSpy, shows us this IL:

.method private hidebysig static 
    void withoutVar () cil managed 
{
    // Method begins at RVA 0x2053
    // Code size 26 (0x1a)
    .maxstack 8

    IL_0000: nop
    IL_0001: ldstr "Before!"
    IL_0006: call void [mscorlib]System.Console::WriteLine(string)
    IL_000b: nop
    IL_000c: br.s IL_000e

    IL_000e: ldstr "After!"
    IL_0013: call void [mscorlib]System.Console::WriteLine(string)
    IL_0018: nop
    IL_0019: ret
} // end of method Program::withoutVar

Which has no recollection of a switch statement anywhere. I think that the reason it doesn't optimize away the second one as well may have something to do with operator overloading and the sort. So, it could be possible that I have a custom type that when assigned to 1, it turns into 2. However, I'm not entirely sure, seems to me like a bug report should be submitted.

Retinol answered 7/3, 2013 at 20:44 Comment(2)
It is not a bug; please do not submit a report.Skeg
@RichardJ.RossIII: Thank you. I'm not sure the what the bug you meant; about the question or about custom type in switch-expression. I test with a custom type which implemented implicit operators from and to int, and intentionally return another value when it assigned 1, seems not the problem. Maybe you are saying some bug else?Lonna

© 2022 - 2024 — McMap. All rights reserved.