Iterating Backwards in C

(davidpriver.com)

82 points | by cremno 3 days ago

19 comments

  • kenferry 5 hours ago
    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!
    • krackers 5 hours ago
      Oh I remember this from the classic SO post, but it was just presented as a funny trick. I never realized that it solves the issue mentioned in the article and there is a legitimate use-case for post-decrement in the comparison itself.

      https://stackoverflow.com/questions/1642028/what-is-the-oper...

      • StilesCrisis 3 hours ago
        It's just spacing. X-- > 0 and X --> 0 are the same thing in C.
      • 1718627440 2 hours ago
        There is a reason postfix is the idiomatic way in C, it is the correct choice most of the time.
        • hirvi74 2 hours ago
          Swift (Lattner) was right, imo, to exclude both.
    • fifilura 18 minutes ago
      And from this day, all C code will look like this because LLMs scraped the most popular SO articles.
    • cryptonector 3 hours ago
      It's a well-known (ish) idiom that nobody uses. It's very entertaining though, for sure. In some circles it's known as the down-to operator.
    • viega 5 hours ago
      I always use unsigned values and do:

        while (i-- != 0) { ... }
      
      It's more clear than the most idiomatic way, which is to drop the explicit comparison:

        while (i--) { ... }
       
      All of it is better than the clunky old for-loop syntax.
      • chii 3 hours ago
        if `i` ever gets modified inside the loop, and skips 0 (e.g., got decremented twice when it is at 1), then you might end up with an infinite loop?

        Usually, this won't happen of course, but because `i` is not declared inside a for-loop, it must have been declared outside, and thus might even look like a regular variable that could be used by some "clever" coding to multipurpose it!

        In any case, it's a niche worry i have, so i usually don't do it if i know there's a for loop that would do.

    • golden-face 3 hours ago
      Praise to the old gods and the new, why?!
    • mrheosuper 2 hours ago
      Code like this is why i prefer human code more than llm.

      Reading code should be enjoyable.

    • Razengan 1 hour ago
      This is why we never escape the cycle of rebirth.
    • mtklein 4 hours ago
      I don't think it's too clever at all. Notably it works with both signed and unsigned i. Frankly if I see anyone doing anything other than this, they're wrong.
    • stephencanon 5 hours ago
      Always.

      (Hi Ken!)

    • hujun236 4 hours ago
      [dead]
  • uecker 1 hour ago
    > 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.

    • david2ndaccount 1 minute ago
      I was unclear in the article, but in my defense I wrote it in a hurry on the couch after seeing the convoluted backwards loop an llm had extruded. I meant if you are actually doing something with the index variable other than indexing. Mixing signed and unsigned types, possibly of different sizes, has surprising behavior to many programmers who don’t have the “usual arithmetic conversions” memorized. If you’re working in size_t the result simplifies: everything becomes size_t. And you generally have size_t anyway as you’re using the result of sizeof or an allocation size passed to your allocation function.

      In general I want to avoid signed types for counts and sizes as then you have to consider both boundary conditions which is easy to mess up.

    • grumbelbart2 1 minute ago
      > True. But I think "int" would be the right choice anyway in almost all case.

      It's the worst choice. int is 32-bit on most 64-bit architecture/compiler-versions that matter.

  • randomblock1 3 hours ago
    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.
    • seeknotfind 3 hours ago
      Yes, this is the best answer. This also works with `-fsanitize=unsigned-integer-overflow`.
  • Joker_vD 3 hours ago
    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 1 hour ago

      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.
  • Panzerschrek 1 hour ago
    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.
  • AdieuToLogic 4 hours ago
    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;
      }
    • i2talics 3 hours ago
      Most compilers will be able to optimize the index version into this.
  • throwaway2037 52 minutes ago
    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;
        > }
  • kccqzy 3 hours ago
    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.)
  • bugake 4 hours ago
    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.

    • StilesCrisis 3 hours ago
      Optimizers eat stuff like this up. It'll restructure your loop however it wants.
      • bugake 56 minutes ago
        You should always check the compiled assembly with Compiler Explorer because compilers sometimes avoid certain optimizations for convoluted reasons, which are hard to predict.
    • sltkr 3 hours ago
      > 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.

      No it's not. It generally makes no difference at all.

      If you disagree, I dare you to write a benchmark that demonstrates a difference between forward and backward iteration on a modern CPU.

      • nkurz 1 hour ago
        Hmm. I think both of you are both right and wrong!

        That is, I claim that while it is no faster to execute a comparison against zero, it still can be faster to finish a loop at zero. This is because internal to the processor, the comparison and the branch are two separate ops. The comparison essentially does an arithmetic operation that sets a flag but throws away the result, and then the branch acts on it.

        But if you finish at zero, the flag you are using is already set by the decrement, so no additional comparison op is necessary. Now normally this doesn't matter, since the processor is multiscalar with sufficient execution ports to fit in an extra comparison with no increase in total cycles spent. But in a tiny and highly optimized loop, occasionally you can get about 20% gain from having one fewer op to execute.

        Getting this to actually happen can be complicated because modern processors do op-fusion that can "fuse" the CMP and branch back into something that can be executed on a single port. But DEC-JNZ can be fused too, which still gives it an advantage. More details on this here: https://chipswiki.org/Macro-operation_fusion

        Now does anyone want to finish the "ground speed readout" routine (https://theaviationgeekclub.com/sr-71-blackbird-pilot-tells-...) by pointing out actually how with modern processors I'm actually wrong too?

      • bugake 1 hour ago
        I quote from "Optimizing Software in C++" by Agner Fog:

        "Comparing an integer to zero is sometimes more efficient than comparing it to any other number. Therefore, it is slightly more efficient to make a loop count down to zero than to make it count up to some positive value, n. But not if the loop counter is used as an array index. The data cache is optimized for accessing arrays forwards, not backwards."

        Yet that text was written for older CPUs. Modern CPUs are better at prefetching backwards access patterns, but if you try a microbenchmark with backwards array traversal, you may get misleading results.

  • MaulingMonkey 5 hours ago
    I prefer:

        const size_t n = 10;
        for (size_t i = n; i --> 0;) {
            std::cout << i << "\n";
        }
    • hedgehog 5 hours ago
      That's a certain kind of taste... I checked out your home page, game dev, check, and this work of art is worth linking (no irony, I love it):

      http://maulingmonkey.com/brainfuck/

      • omoikane 4 hours ago
        That looks like the brainfuck mandelbrot set by Erik Bosman, but the example is missing the header line with the credit. The one I saw came from here:

        https://github.com/thaliaarchi/bfcorpus/blob/main/ebosman/ma...

        Be sure to check out other files in that directory for how the program is assembled.

        (Erik Bosman's mandelbrot set is a personal favorite of mine, very useful for benchmarking brainfuck interpreters)

    • ranger_danger 5 hours ago
      Cute but wouldn't work with ssize_t :)

      VHDL's array/vector syntax actually works a similar way:

      -- gives 0, 1, 2, 3:

      for i in 0 to 3 loop

      -- gives 3, 2, 1, 0:

      for i in 3 downto 0 loop

      -- Source - https://stackoverflow.com/a/7642026 -- Posted by Kerrek SB -- Retrieved 2026-10-29, License - CC BY-SA 3.0

      • fourthark 5 hours ago
        Does work with ssize_t, just not necessary.
        • ranger_danger 4 hours ago
          Depends on your definition of "work".

          For me it does not work because I'd want to use it with values < 0, but the for loop checks for > 0, so I would have to modify that first.

  • kooi 3 hours ago
    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]
    
    }
    • sltkr 3 hours ago
      This fails on overflow: if N == INT_MAX then your loop doesn't terminate and actually invokes undefined behavior in C.

      The safe way to write it is:

          for (int i = 0; i < N; ++i) {
              something(array[N - 1 - i]);
          }
      
      (or with size_t instead of int depending on the type of N)
  • ycui7 1 hour ago
    too bad, no one write code anymore. a nice trick sadly become irrelevant .
  • abstractspoon 3 days ago
    I've been using this method for more than 30 years!
  • kevin_thibedeau 3 hours ago
    > 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.

    • sltkr 3 hours ago
      No they aren't.
      • Joker_vD 1 hour ago
        Well, the array indices are allowed to be any sorts of integers, and the standard is worded in such a way that such addition must work correctly without overflowing — provided, of course, that the array you're indexing into is large enough. So in practice, you're bounded by the width of size_t.

        Oh, and did you know that the width of ptrdiff_t is not required to have any relation to the width of size_t? There is even an explicit provision in standard for the case when subtracting two pointers into the same very large array can't fit into ptrdiff_t:

            When two pointers are subtracted, both shall point to elements of the same array object, or one past
            the last element of the array object; the result is the difference of the subscripts of the two array
            elements. The size of the result is implementation-defined, and its type (a signed integer type) is
            ptrdiff_t defined in the <stddef.h> header. If the result is not representable in an object of that
            type, the behavior is undefined.
        
        So yeah; you should try to work strictly with indices only, they're guaranteed to be correctly addible to pointers no matter the sizes of your arrays.
  • lanmu 4 hours ago
    [flagged]
  • rpsworld 5 hours ago
    [flagged]
  • ReDress 3 hours ago
    [dead]
  • Mikhail_Edoshin 1 hour ago
    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.