Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

The behavior of the for loop split between switch cases

While playing with the code I've noticed a strange behavior that I do not know to explain the logic behind

void foo(int n)
{
    int m = n;
    while (--n > 0)
    {
        switch (n)
        {
            case -1:
            case 0:
                for (int j = 0; j < m; ++j)
            default:
                    printf(":-)");
                break;
        }
    }
}

int main()
{
    foo(10);
    return 0;
}

I would expect the printf to execute let's say 10 times. Then I saw it continues to run (imagine 100000 instead of 10) and supposed that the developers (VS) interpreted the printf inside the for (pretty expected), hence the output is made n times for each entrance to switch.

But then turned out j was never initialized.

So my question is why ? Is this an undefined behavior? Is not this a, supposedly, standard code?

like image 229
dEmigOd Avatar asked Mar 08 '21 07:03

dEmigOd


5 Answers

default is just a label (address to where the code jumps if n is not -1 or 0). Thus when n is not -1 or 0, the flow enters the body of the for loop skipping the initialisation of j. You can write the same code also as this, so it would be clearer what is happening here:

int m = n;
while (--n > 0)
{
    switch (n)
    {
        case -1:
        case 0:
            for (int j = 0; j < m; ++j)
            {
        default: printf(":-)");
            }
            break;
    }
}

(Note, as mentioned by @alagner in the comments, it won't compile with C++ compiler but perfectly compiles with C one so this is good enough to make my point and explain how the code looks).

So yes, since j is uninitialised, it is undefined behaviour. If you enable compiler warnings, it will warn you about it (https://godbolt.org/z/rzGraP):

warning: 'j' may be used uninitialized in this function [-Wmaybe-uninitialized]
   12 |                 for (int j = 0; j < m; ++j)
      |                          ^                  
like image 85
Alex Lop. Avatar answered Oct 20 '22 15:10

Alex Lop.


A switch block is effectively a glorified set of goto statements. The different cases don't introduce scopes or any logical structure to the code. They're really just targets for the switch statement to jump to.

In this program, the default: label is inside a nested for loop. When the default case is hit the program jumps inside the loop as if there were a goto statement there. The switch block is equivalent to:

if (n == -1 || n == 0) {
    goto before_loop;
}
else {
    goto inside_loop;
}

before_loop:
for (int j = 0; j < m; ++j) {
    inside_loop:
    printf(":-)");
}

This is dangerous because jumping to inside_loop: skips over j = 0. As you've observed, j is still declared but it's not initialized, and accessing it leads to undefined behavior.

like image 11
John Kugelman Avatar answered Oct 20 '22 17:10

John Kugelman


As posted, the code has undefined behavior because when the switch jumps to the default: label, inside the for statement body, it skips the initialization of j in the inner loop, causing undefined behavior when j is tested and decremented when the loop iterates.

Skipping the initialization with a direct jump into a new scope is not allowed in C++. Such a constraint is not present in the C language, for compatibility with historical code where is does not necessarily cause problems, but modern compilers detect this error and complain. I recommend using -Wall -Wextra -Werror to avoid silly mistakes.

Note that modified as below, it becomes fully defined, prints :) 90 times (9 iterations of the outer loop, times 10 iterations of the inner loop) and finishes successfully:

#include <stdio.h>

void foo(int n) {
    int m = n;
    while (--n > 0) {
        int j = 0;
        switch (n) {
            case -1:
            case 0:
                for (j = 0; j < m; ++j)
            default:
                    printf(":-)");
                break;
        }
    }
}

int main() {
    foo(10);
    printf("\n");
    return 0;
}
like image 7
chqrlie Avatar answered Oct 20 '22 17:10

chqrlie


For starters, the function foo having the return type int returns nothing.

The while loop:

while (--n > 0)
{
    //..
}

gets control only in the case when the value of the pre-decrement expression --n is greater than 0.

That is, within the while loop, the variable n is neither equal to 0 nor to -1.

So, control will be passed at once to the label default within the switch statement.

    switch (n)
    {
        case -1:
        case 0:
            for (int j = 0; j < m; ++j)
        default:
                printf(":-)");
            break;
    }

You may equivalently rewrite the while loop without the switch statement the following way to make it clearer:

while (--n > 0)
{
    goto Default;

    for (int j = 0; j < m; ++j)
    {
        Default: printf(":-)");
    } 
}

That is, control is at once passed inside the for loop. According to the C Standard (6.8.5 Iteration statements)

4 An iteration statement causes a statement called the loop body to be executed repeatedly until the controlling expression compares equal to 0. The repetition occurs regardless of whether the loop body is entered from the iteration statement or by a jump.

It means that the for loop will contain one statement:

printf(":-)");

will be executed.

However, the initial initialization of the variable j in the for loop is bypassed. From the C Standard (6.2.4 Storage durations of objects)

6 For such an object that does not have a variable length array type, its lifetime extends from entry into the block with which it is associated until execution of that block ends in any way. (Entering an enclosed block or calling a function suspends, but does not end, execution of the current block.) If the block is entered recursively, a new instance of the object is created each time. The initial value of the object is indeterminate. If an initialization is specified for the object, it is performed each time the declaration or compound literal is reached in the execution of the block; otherwise, the value becomes indeterminate each time the declaration is reached.

So, the variable j has indeterminate value. It means that the for loop and, as a result, the function itself, have undefined behavior.

like image 5
Vlad from Moscow Avatar answered Oct 20 '22 15:10

Vlad from Moscow


Lots of good explanations, but the key point is missing: that the compiler places a jmp instruction after the printf statement because it just compiled a for statement. The jmp jumps to the condition of the loop and continues there (using an uninitialized j).

like image 3
Paul Ogilvie Avatar answered Oct 20 '22 16:10

Paul Ogilvie