logoalt Hacker News

Iterating Backwards in C

103 points • by cremno • last Monday at 7:28 PM • 65 comments • view on HN

Comments

xg15 • today at 7:18 AM

> for(size_t i = count; i--; ) { /* find needle */ }

As someone who only writes C or C++ occasionally, I'd consider this a good submission for an obfuscated C contest.

It really does look simple at first glance.

Then you start to wonder what the stop condition is.

Then you realize the i-- is the stop condition.

Then you wonder how the whole thing works at all.

(The description explained the trick well, and the construct makes sense once you understand it, but I still think this is one example of something that is "too simple" and becomes cryptic)

After all the stuff you hear about undefined behavior, I'm also honestly surprised that wraparound on unsigned types is not undefined behavior.

➕ show 1 reply
kenferry • today at 1:31 AM

I know it's "too clever", but if I'm writing code no one else will ever see,

  while (i --> 0) {
      // use array[i]
  }
easy to read (if not understand), and correct!
➕ show 10 replies
uecker • today at 5:48 AM

> Now your index variable is signed, which can cause problems when used in expressions due to C's conversion rules.

I don't see this. What problems would those be?

> ssize_t is a POSIX thing so you'd have to define it yourself on windows.

True. But I think "int" would be the right choice anyway in almost all case.

> If your type is 32 bit, you run into promotion problems and might even run into truncation problems.

What promotion problems? If your loop exceeds 2G than yes, you run into overflow (not for backwards loop though). But with a signed type you can automatically get a trap or diagnostic for overflow. This is far easier to debug and protect against compared to mistakes with unsigned types.

glum64 • today at 8:37 AM

There is no right or wrong, only what works and what does not.

Obviously the author never fixed 10 y.o. bugs at 0430 hrs in a room full of people watching his every finger. In moments like that, the programmer wants to work with the most boring, straightforward code.

Artistry contests is sometging else. I like doing this (iteration [0..9] backwards):

size_t i = 9;

for ( ; i < 10; i -- ) ...

➕ show 1 reply
randomblock1 • today at 3:10 AM

I thought most people used the explicit version:

  for (size_t i = count; i > 0; --i) {
      // use array[i - 1]
  }
You can also do something like:

  size_t index = i - 1;
to not have to use i-1. I believe the compiler optimizes that out anyway to be identical.
➕ show 1 reply
Joker_vD • today at 3:18 AM

That's the price of innovation, I guess; before C invented its "for"-loop, other languages had their equivalents generally looking like this:

    FOR var = start [DOWN] TO end BY step DO ... END
No custom increment expressions, and "step" even usually was only allowed to be a constant (in which case, the language usually didn't have DOWN specifier and used the step's sign instead). Then Dennis Ritchie invented C, and ditched the traditional "do"-loop (Fortran started that construct with DO, so it was called DO loop; Algol used FOR instead, but most people preferred the Fortran's nomenclature... until C came to dominate, but that wouldn't happen for yet another 20 years) in favour of his invention because he felt that easily iterating through linked lists or strings was a much more pressing concern:

    for(p=head; p; p=p->next) { ... }

    for(s=str; *s; s++) { ... }
The downside, of course, is that now you can't straightforwardly write

   for size_t i = count - 1 downto 0 do
      ...
   end
cv5005 • today at 5:51 AM

  for(size_t i = count-1; i < count; --i)
'i < count' always works for unsigned, it's why I prefer unsigned for indices, only one invariant to maintain.
childintime • today at 9:05 AM

> for(size_t i = count - 1; i >= 0; i--){

> i >=0 is tautologically true (modern compilers will warn).

Note that a tautology in language design is an opportunity. Here the compiler could see what the programmer meant and do the right thing using the overflow flag (though the way this is written doesn't favor this).

I'm not recommending, I'm just saying compilers traditionally have just thrown their hands in the air, kicked the developer around with a stupid error message and called it a day. Apparently only recently they started emitting a warning.

The "best" developer was the one who took the abuse the "best tools" gave him in stride. Nowadays, in their "good faith", compilers don't warn you when they detect undefined behavior. They then go on to reinterpret your code, to show you they read the standards book and you didn't. They never questioning if the standards are wrong or out of date, they don't give you a optional warning like "maybe you didn't intend this, but i'm doing things behind your back?". They placate to the legalistic people that don't actually do the job.

The amount of abuse the developer is willing to take has been the self selecting bias in the industry for many decades. It's made us the heroes of the misplaced comma, changing the meaning of a sentence. A symptom is the lack of women in our profession. I welcome AI for liberating us from what's been hell more often than not.

kccqzy • today at 3:29 AM

This is why I prefer to use C++ and its reversed iterators. The rbegin and rend accessors are there in the containers from the beginning. It’s less error prone than the C version using indices. (Of course some situations require me to use indices, so I do use the i-->0 trick, but it is much rarer.)

AdieuToLogic • today at 2:25 AM

Why not use the C/C++ idiom of pointer math?

  _Bool find_last (size_t count, int *haystack, int needle, size_t *where)
  {
    for (const int *pos = haystack + count; pos-- != haystack;) {
      if (*pos == needle) {
        *where = pos - haystack;
        return 1;
      }
    }
    return 0;
  }
➕ show 1 reply
MaulingMonkey • today at 1:14 AM

I prefer:

    const size_t n = 10;
    for (size_t i = n; i --> 0;) {
        std::cout << i << "\n";
    }
➕ show 2 replies
Panzerschrek • today at 5:07 AM

I do this in another way:

  for( size_t i_plus_one = count; i_plus_one > 0; --i_plus_one )
  {
      const size_t i= i_plus_one - 1;
      // actual loop code
  }
It's in my opinion better than using i-- as condition, since I generally avoid using results of -- and ++, because doing so is bug-prone.
kooi • today at 3:16 AM

As long as you make sure the end points are valid, this is most intuitive for me. Just using i as a backwards step.

for (int i = 1; i <= N; i++) {

    array[N-i]

}
➕ show 1 reply
bugake • today at 2:44 AM

For the uninformed: counting backwards in a for loop is faster because comparing an unsigned integer against 0 is generally faster than comparing it against any other value.

It also says not to use `for (i >= 0)` because any unsigned integer is >= 0, and the loop will never end.

➕ show 2 replies
throwaway2037 • today at 6:00 AM

His final solution is so tricky. I needed to re-read it a few times to understand the trick of using post decrement in the for loop condition. Personally, I would never write code like that. I would always find a way to use ssize_t.

This part was an interesting requirement:

    > ssize_t is a POSIX thing so you'd have to define it yourself on windows.
That seems like a lot of mental gymnastics to avoid this very simple macro:

    > #if defined(_WIN32) && !defined(ssize_t)
    > #define ssize_t SSIZE_T
    > #endif
Read about the Win32 macro SSIZE_T here: https://learn.microsoft.com/en-us/windows/win32/winprog/wind...

To me, this solution is more readable and maintainable:

    > for (ssize_t i = count - 1; i >= 0; i--)
    > {
    >     if (haystack[i] == needle)
    >     {
    >         *where = i;
    >         return 1;
    >     }
    > }
    > return 0;
Alternatively, you can use a while loop where the condition appears at the end of the loop:

    > size_t i = count - 1;
    > while (1)  // true
    > {
    >     if (haystack[i] == needle)
    >     {
    >         *where = i;
    >         return 1;
    >     }
    >     if (0 == i)
    >     {
    >         return 0;
    >     }
    >     --i;
    > }
ycui7 • today at 5:42 AM

too bad, no one write code anymore. a nice trick sadly become irrelevant .

abstractspoon • last Tuesday at 6:35 AM

I've been using this method for more than 30 years!

kevin_thibedeau • today at 3:37 AM

> Now your index variable is signed, which can cause problems when used in expressions due to C's conversion rules.

Array indices are already signed. This shouldn't be a problem.

➕ show 1 reply
flohofwoe • today at 7:35 AM

Eh I don't know, the straightforward solution is still to use a signed loop counter (and signed indices) and only ever use unsigned integer types for bit twiddling. Also, enable `-Wsign-conversion` (it's not in -Wall -Wextra) to catch all the places where the compiler implicitly converts between signed and unsigned behind your back.

TL;DR: "alomost always signed!" :)

lanmu • today at 2:07 AM

[flagged]

rpsworld • today at 1:10 AM

[flagged]

ReDress • today at 3:35 AM

[dead]

Mikhail_Edoshin • today at 5:32 AM

I myself write loops with goto. It gives precise control, allows to reuse the knowledge gained from operations inside the loop, avoids artificial "while true" and is generally more convenient. It is also makes the language simpler as you can forget 4 to 5 loop-related keywords and hard-to parse operators designed primarily to look nice in loop conditions while also modifying their arguments.