Infinite for loop

The question is, why the loop below falls into an infinite cycle.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

void infinite() {
    unsigned char max = 160;  // very large
    for (signed char i = 0; i < max; ++i) std::cout << int(i) << '\n';
}

int main()
{
    infinite();

    system("pause");
    return 0;
}

My answer to that is: The character char on line 4 is unsigned therefore it can embrace numbers from 0 to 255 based on its one byte size. The signed char in the for-loop, i, on the other hand, can't save numbers upper than 127 (since its range is -2^7 to 2^7-1).

Now what happens when the loop falls into an infinite cycle: When i reaches 127 and is less than max, it's then incremented to be 128, it fails in reaching that number and goes back to the least significant value which is -128 which is less yet than max. This way, the loop goes on forever.
Last edited on
i incrementing to what would be 128 invokes undefined behavior, because [signed] integer overflow is undefined behavior. But yes, in this case, it happens to do the simplest thing (cpu-wise) and revert back to -128 and repeat.
Last edited on
Stroustrup somehow says:

The ++ is modulo arithmetic, so after the largest number that can be
represented we get 0 for an unsigned integer (and the loop terminates). If the type is a signed integer (e.g., signed char), the numbers will suddenly turn negative and start working their way back up to 0 (where the loop will terminate). For example, for a signed char, we will
see 1 2 . . . 126 127 –128 –127 . . . –2 –1.


I think it's an erratum in the book and for the signed int he should have written: ... (where the loop will continue)

What page and what edition?

What Stroustrup is describing is in fact what is happening, but in my opinion it is careless, because he writes, "If Int is a signed integer (e.g. signed char), the numbers will suddenly turn negative ..." but doesn't clarify that this behavior is undefined.

https://en.cppreference.com/w/cpp/language/operator_arithmetic
When signed integer arithmetic operation overflows (the result does not fit in the result type), the behavior is undefined, — the possible manifestations of such an operation include:

it wraps around according to the rules of the representation (typically 2's complement),
it traps — on some platforms or due to compiler options (e.g. -ftrapv in GCC and Clang),
it saturates to minimal or maximal value (on many DSPs),
it is completely optimized out by the compiler.


The last possibility is the most interesting one. Look at this example (assembly from godbolt) to understand:
1
2
3
4
bool is_max_value(signed char ch)
{
    return (ch + 1 < ch);
}

https://godbolt.org/z/8xTb36

If signed char were guaranteed to wrap back around to -128, then an input of 127 would add 127 + 1, and then would become -128, and -128 < 127 would return true.

However, with optimization turned on, this function becomes:
1
2
3
is_max_value(signed char):
        xor     eax, eax
        ret

(This just means "return false, always")

So the compiler optimized away the signed char integer overflow by assuming it can never happen.

PS: In your quote, the first sentence starts with, "If Int [some user-defined type] is unsigned (e.g., unsigned char, unsigned int, or unsigned long long), the ++ is modulo arithmetic, ...". (The way you had it unqualified made it sound like the ++ operator is always 2^n modulo arithmetic.)
Last edited on
1) Don't the first three possibilities look close to each other in meaning or even the same?

2) For the code, what if we use the snipped-code below?

1
2
3
4
5
bool is_max_value(signed char ch)
{
    char t = ch;
    return (++ch < t);
}


3)
What Stroustrup is describing is in fact what is happening
But based on my testing on Windows and using VS 2019, the loop for the signed int doesn't terminate; it continues!
Last edited on
1)
- Wrapping around 2's complement is the most likely scenario, which is what Stroustrup described.
- A trap is a special type of OS-level exception (it could be silently handled and just wrap). I'm not too familiar with these so I won't comment.
- Saturation means that adding 1 to a signed char value of 127 will just stay as 127, for instance.
Going down the rabbit hole of all the ways the implementation could possibly materialize the undefined behavior is frankly an exercise in futility.

2) I tried the code with gcc and clang. Neither appear to optimize it to a simple return false anymore. In fact, clang's assembly is the clearest and just does a compare to 127 and return. This again demonstrates how fragile code with undefined behavior is.

3) Yes, I am agreeing that is what is happening, and that is what will most likely happen on most implementations. My point is that signed integer overflow is not defined behavior and should not be relied on.
Last edited on
Topic archived. No new replies allowed.