defining a variable inside a switch() statement produces 'strange' behaviour

Hello all

  1. When running the sample program below ("Minimal, Reproducible Example"):
void setup()
{
    Serial.begin(128000);
    while (!Serial) {}

    for (int i = 1; i <= 2; i++) {
        switch (i) {
            case 1: {
                long j = 100;
                Serial.println("case 1");
                break;
            }
            case 2: {
                Serial.println("case 2");
                break; 
            }
        }
    }
}

I get this result on the serial monitor:
case 1
case 2
which is as I expected.

  1. When removing the curly braces around the two case clauses, like this:
void setup()
{
    Serial.begin(128000);
    while (!Serial) {}

    for (int i = 1; i <= 2; i++) {
        switch (i) {
            case 1: 
                long j = 100;
                Serial.println("case 1");
                break;
            
            case 2: 
                Serial.println("case 2");
                break; 
            
        }
    }
}

I get this result on the serial monitor:
case 1
which is not as I expected.

  1. When I move the definition of the long variable j (which is not used further on in this sample program, but this doesn't matter) to a point before the switch statement, like this:
void setup()
{
    Serial.begin(128000);
    while (!Serial) {}

    for (int i = 1; i <= 2; i++) {
        long j;
        switch (i) {
            case 1: 
                j = 100;
                Serial.println("case 1");
                break;
            
            case 2: 
                Serial.println("case 2");
                break; 
            
        }
    }
}

I again get this result on the serial monitor:
case 1
case 2
which is again the behaviour I expected.

Am I overlooking something, or could it be that sample program 2, above (the combination of removing the curly brackets and defining a long variable inside the 'case 1' clause, as I explained) could possibly point to a compiler issue or something of that nature ? I couldn't find any elements in the C++ documentation explaining this behaviour.

Many thanks

Did you note the warning message when compiling your second example ?

C:\Users\Bob\AppData\Local\Temp\arduino_modified_sketch_18669\sketch_jan31c.ino:13:14: note:   crosses initialization of 'long int j'

         long j = 100;

Earlier versions of the IDE reported that as an error.

The lesson is, put the code block for any case in which a local variable is declared in curly brackets

I'm using Visual Studio 2019 (I should have mentioned it, sorry).
The compiler gives me 0 errors and 0 warnings.
We both understand what to do to make sure the behaviour is as expected (you mentioned it and I already came to the same conclusion).
But my question remains: is this explainable ? Do I violate any syntax rules ?

Thanks

UKHeliBob:
The lesson is, put the code block for any case in which a local variable is declared in curly brackets

What Bob said. The compiler is trying to protect you from scoping/initialization issues.

The issue has to do with the case statements being labels, the scope of the variable you are defining, and initialization of the variable. It is not worth drowning in the gory details so the put the declaration in a code block in order to limit the scope of the variable and ensure proper initialization.

ToddL1962:
It is not worth drowning in the gory details so the put the declaration .....

Curious opinion

bidouilleelec:
Curious opinion

Why???

It is indeed a curious opinion (thanks Bidouilleelec).
We already know how to avoid the issue. So, that's not my question.
My question remains: why does the issue (in sample 2) occur ?
If you don't want to explain it to me: fine.
If you don't know the details: also fine.
But I want to understand - is there anything wrong with that ?
Isn't that one of the purposes of this forum ?
Because, with a compiler not giving any warnings or errors, and with these versions of a program all being syntactically correct, it occurs to me that these versions would have to produce the same result.
Scary if they don't, and "we don't have to know why".
Thanks for enlightening me.

I can't comment on VS compilers, but in your second example, j is in scope in case 2, but it is uninitialised.
Why it produces the result you see, I don't know.

Try this - let us know

    for (int i = 1; i <= 2; i++) {
        switch (i) {
            case 1:
            {
                long j = 100;
                Serial.println("case 1");
                break;
            }
            case 2:
            {
                Serial.println("case 2");
                break;
            }
        }

herwig9820:
I'm using Visual Studio 2019 (I should have mentioned it, sorry).
The compiler gives me 0 errors and 0 warnings.
We both understand what to do to make sure the behaviour is as expected (you mentioned it and I already came to the same conclusion).
But my question remains: is this explainable ? Do I violate any syntax rules ?

Thanks

Are you using Arduino IDE plugin? If so you still need the arduino ide (what version?)
Your code should at least produce a warning: "j is unused".

If anything, the VS compiler is the most strict compiler I have worked with.

And, arduino code does not use VS compiler

There are some good discussions on Stack Exchange that get into this issue and explain it:

As for your specific question about your particular compiler not choking on it, not sure.

Per the above link from the discussion thread in Stack Exchange, see below for a slight modification to your #2 code that should both compile and run (even if not "best practice" in terms of managing variable scopes, etc).

The explanation given at Stack Exchange for why your #2 code doesn't work is that "In C++ this code is invalid because the case label [Case 2 in your situation] jumps into the scope of variable bypassing its initialization. Jumps that bypass initialization of automatic objects are illegal in C++."

void setup()
{
    Serial.begin(128000);
    while (!Serial) {}

    for (int i = 1; i <= 2; i++) {
        switch (i) {
            case 1:
                long j;    //* this line changed. Should now work.
                j = 100;    //* this line changed. Should now work.
                Serial.println("case 1");
                break;
           
            case 2:
                Serial.println("case 2");
                break;
           
        }
    }
}

Sure smells to me of a compiler bug. What VERSION IDE or compiler is this? 1.8.10? That one seems to have plenty of bugs. I've used 1.8.9 for most of the last year with no problems.

Regards,
Ray L.

Without the curly braces, you have "undefined behaviour". This sounds like a squishy concept, but is elaborated upon quite fully in the C++ standard.

The reason the standard has this idea is because the language syntax allows you to get into situations where there is no agreement as to what the 'right' answer is. Like the above - if you skip over the initialization, what should the value of j be?

Here's the thing - if your program has undefined behaviour, the compiler is allowed to do anything at all. It doesn't have to diagnose it, it doesn't have to produce reasonable code, it can do anything.

In this case, the compiler is probably removing the code in the optimizer. Since a jump to case 2 skips over initialization, it's undefined. Ergo, the compiler can just ignore that case completely. Bam, gone! The most optimized code is the code that doesn't get to run at all!

Here's a link to info about undefined behaviour:
https://en.cppreference.com/w/cpp/language/ub

MHotchin:
Without the curly braces, you have "undefined behaviour". This sounds like a squishy concept, but is elaborated upon quite fully in the C++ standard.

The reason the standard has this idea is because the language syntax allows you to get into situations where there is no agreement as to what the 'right' answer is. Like the above - if you skip over the initialization, what should the value of j be?

Here's the thing - if your program has undefined behaviour, the compiler is allowed to do anything at all. It doesn't have to diagnose it, it doesn't have to produce reasonable code, it can do anything.

In this case, the compiler is probably removing the code in the optimizer. Since a jump to case 2 skips over initialization, it's undefined. Ergo, the compiler can just ignore that case completely. Bam, gone! The most optimized code is the code that doesn't get to run at all!

Here's a link to info about undefined behaviour:
Undefined behavior - cppreference.com

No, that's not it. This code is very well defined, in that the compiler threw error/warning.
The OP claimed that the bad code didn't generate error/warning without evidence.

arduino_new:
No, that's not it. This code is very well defined, in that the compiler threw error/warning.
The OP claimed that the bad code didn't generate error/warning without evidence.

The compiler emitting a warning does NOT make the behaviour well defined. Case 2 skips over initialization, THEREFORE the behaviour is undefined. There are no other options or arguments here, the C++ spec is very very specific that this is undefined behaviour. It's even one of the examples in the link I provided!

It's nice that the compiler tried to tell you about it, but it's not required to do so, and telling you about it does NOT make the behaviour well defined.

arduino_new:
No, that's not it. This code is very well defined, in that the compiler threw error/warning.
The OP claimed that the bad code didn't generate error/warning without evidence.

If that is true, then there is no problem. My understanding was the OP claimed there was no error, no warning. I would expect one or the other, and if either is generated, then all is good. If the OP chooses to ignore an error or warning.... Well, that is on him, not the compiler.

I know for a fact the 1.8.9 compiler WILL generate "error: crosses initialization". Has this changed in 1.8.10, or whatever version OP is using?

Regards,
Ray L.

MHotchin:
The compiler emitting a warning does NOT make the behaviour well defined. Case 2 skips over initialization, THEREFORE the behaviour is undefined. There are no other options or arguments here, the C++ spec is very very specific that this is undefined behaviour. It's even one of the examples in the link I provided!

It's nice that the compiler tried to tell you about it, but it's not required to do so, and telling you about it does NOT make the behaviour well defined.

Pretty sure this is a well defined behavior.

It is possible to transfer into a block, but not in a way that bypasses declarations with initialization. A program that jumps from a point where a local variable with automatic storage duration is not in scope to a point where it is in scope is ill-formed unless the variable has POD type (3.9) and is declared without an initializer.

arduino_new:
Pretty sure this is a well defined behavior.c++ - Getting a bunch of crosses initialization error - Stack Overflow

Nope, j is initialized to value 100 in this case. You can jump into scope if the variable is uninitialized, but that seems like a bad idea in so many other ways.

arduino_new:
Pretty sure this is a well defined behavior.

The text you quoted directly contradicts that:

A program that jumps from a point where a local variable with automatic storage duration is not in scope to a point where it is in scope is ill-formed

(Emphasis mine.)

MHotchin:
Nope, j is initialized to value 100 in this case. You can jump into scope if the variable is uninitialized, but that seems like a bad idea in so many other ways.

christop:
The text you quoted directly contradicts that:

(Emphasis mine.)

Undefined means that it is not defined by the standard. If the standard defined it (in this case, the program is ill-formed which is an error), then the behavior is well-defined.