Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

Welcome to Software Development on Codidact!

Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.

Why is a for loop getting stuck when using a uint64_t counter, whereas a while loop isn't?

+1
−0

I asked this question a while ago over SE.


When I use a for loop with a uint64_t as a counter, it gets stuck forever, even though the condition seems to be well defined.

Offending MCVE

#include <stdio.h>
#include <inttypes.h>

int main() {
    uint64_t i;
    for (i = 15; i >= 0; i--) { printf("%"PRIu64" ", i); }
    return 0;
}

Partial output

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 18446744073709551615 18446744073709551614 18446744073709551613 18446744073709551612 18446744073709551611 18446744073709551610 18446744073709551609 18446744073709551608 18446744073709551607 18446744073709551606 18446744073709551605 18446744073709551604 18446744073709551603 18446744073709551602 18446744073709551601 18446744073709551600 18446744073709551599 18446744073709551598 18446744073709551597 18446744073709551596 18446744073709551595 18446744073709551594 18446744073709551593 18446744073709551592 18446744073709551591 18446744073709551590 18446744073709551589 18446744073709551588 18446744073709551587 18446744073709551586 18446744073709551585 18446744073709551584 18446744073709551583 18446744073709551582 18446744073709551581 18446744073709551580 18446744073709551579 18446744073709551578 18446744073709551577 18446744073709551576 18446744073709551575 18446744073709551574 18446744073709551573 18446744073709551572 18446744073709551571 18446744073709551570

It seems it's ignoring the stop condition, and so it rolls over.

However, when changing it to an "equivalent" while loop, everything works fine:

Correct MCVE

#include <stdio.h>
#include <inttypes.h>

int main() {
    uint64_t i = 16;
    while (i--) { printf("%"PRIu64" ", i); }
    return 0;
}

Complete output

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 

Am I missing something regarding the use of uint64_t counters in a for loop? Any help is greatly appreciated!

History
Why does this post require attention from curators or moderators?
You might want to add some details to your flag.
Why should this post be closed?

0 comment threads

3 answers

+5
−0

This is a bit of a well-known problem when converting from an up-counting to a down-counting loop and using an unsigned loop iterator.

Since unsigned numbers are always positive and have well-defined wrap-around when going below zero, i >= 0 will always be true for an unsigned loop iterator. Compilers often warn against this.

It has nothing to do with for vs while, the comparison expressions used are simply not equivalent. while(i--) is the same as while(i-- != 0) so it performs no range check of the value with relative operators, only an equivalence check. The equivalent for loop would be

for(int i=16; i--; )

Which is strange and bad practice but valid C.

As for how to fix it, one solution I often recommend is don't write a down-counting loop. The best and most readable loop we can write is the idiomatic:

for(int i=0; i<n; i++)

The further we divert from that form, the harder the loop is to read and bugs become more probable. We can actually write your example with an up-counting loop:

for(int i=0; i<n; i++)
{
  printf("%"PRIu64" ", n-i-1);
}

By subtracting the loop iterator from the upper limit and then an additional -1, we can achieve the same backwards counting. The loop body looks a bit stranger but now it actually doesn't matter if we use a signed or unsigned loop iterator. And it has the pretty nice advantage that it actually works :)

Alternatively, if we insist on using down-counting with an unsigned iterator, there's not really a pretty way to do it either:

  • We could increase the starting value by 1 and change the condition i>0. This might mean we have to adjust the loop body too, since indices end up off by 1.
  • We could leave out the for loop condition and add a if(i==0) break; at the end of the loop.
  • We could let the iterator deliberately wrap-around and write a strange loop condition like
    i != UINT64_MAX or i != (uint64_t)-1, which looks plain weird.
  • We could cast the iterator (int64_t)i >= 0 which is somewhat more readable but introduces implementation-defined behavior to the code.

So the best solution here probably is to drop one of the requirements: either don't write down-counting loop or don't use an unsigned loop iterator.


Side note:

Note that down-counting loops are not faster. It used to be like that back in the days when compilers were horrible at optimization. Namely because an assembler instruction "branch if zero" is usually a few ticks faster than "branch if lower than n" or similar checks that uses a comparison against a number.

However, all modern compilers are well-aware of this and can turn a loop down-counting in the generated machine code if that would make the code faster. So writing down-counting loops on purpose would be a school book example of "premature optimization" these days.

History
Why does this post require attention from curators or moderators?
You might want to add some details to your flag.

0 comment threads

+0
−0

SE user DU Jiaen provided the following answer, edited by Martin Zabel and found here.


The expression i >= 0 is always true if i is of an unsigned integer type. The alternative and simple way is to change it into:

uint64_t i;
for (i = 15; i != -1; i--) { ... }

This works no matter if i is a signed or an unsigned integer. In the uint64_t case, -1 will first get converted into 0xFFFFFFFFFFFFFFFF and then compared with i.

If you want to get rid of the compile warning, change it into:

i != (uint64_t)-1

but you need to ensure the uint64_t is exactly the type of i.

History
Why does this post require attention from curators or moderators?
You might want to add some details to your flag.

1 comment thread

i != -1 is brittle (1 comment)
+0
−0

SE user M Oehm provided with the following answer, as it can be found here.


The condition i >= 0 is always true if i is an unsigned type. Decrementing an unsigned zero will not produce a negative value, but the counter will wrap to the maximum representable number for the given type.

C represents ranges with an inclusive lower bound and an exclusive upper bound. For example, an array of N elements has indices 0 through N - 1. The upper bound itself isn't a valid array index.

This convention means that you use a value before incrementing it, but decrement it before using it. Consider a simple stack:

stack[nstack++] = x;      // push a value
x = stack[--nstack];      // pop a value

The same logic goes for loops: When you move forwards, use the value before you increment it:

for (var i = 0; i < N; i++) { use(i); }

When you move backwards, decrement first and then use it:

for (var i = N; i-- > 0; ) { use(i); }

This loop is equivalent to your while. The update section, which happens after processing the body, is empty here. The check is performed on the value before entering the loop; the loop body has the updated value.

This backwards loop might look awkward with the empty update section, but in other ways it is orthogonal to the forward version:

  • It uses the actual bounds; there's no need to start with N - 1;
  • It works equally well for arbitrary bounds as long as they follow the convention of inclusive lower and exclusive upper bound;
  • The test is a pure inequality, not a greater/less-then-or equal comparison.
History
Why does this post require attention from curators or moderators?
You might want to add some details to your flag.

0 comment threads

Sign up to answer this question »