16
$\begingroup$

There are two kinds of for loops: the kind that Python has, also called a "foreach":

for i in [1, 2, 3]:
  print(i)

and the kind that C has, which is syntactically heavy:

int arr[] = { 1, 2, 3 };
for (int i = 0; i < 3; ++i)
  printf("%d\n", arr[i]);

Some languages, such as C#, have both, but I've noticed that many modern languages only have the foreach.

What are some advantages and disadvantages to allowing each kind?

$\endgroup$
1

8 Answers 8

11
$\begingroup$

C-style loops can allow for arbitrary increments

This is especially visible when the increment is some non-constant calculation, like when I was trying to implement an optimized prime checker in Kotlin, which doesn’t have C style for loops. As noted in the comments, you can also go back (have a negative increment) using C-style for loops.

$\endgroup$
3
  • 2
    $\begingroup$ Similarly, they also allow you to go back on your loop, that is to modify your iteration variable in the middle of execution. IIRC bubble-sort is slightly quicker if you go back 1 iteration after swapping values? $\endgroup$
    – kouta-kun
    May 16, 2023 at 23:56
  • 1
    $\begingroup$ Janet has a forv function that is like a normal for range loop. It still automatically increments i, but it's a C-style for loop internally, and you can still modify i. $\endgroup$
    – naffetS
    May 18, 2023 at 20:27
  • 1
    $\begingroup$ This is nice when you've got variable length sequences packed into an array. For example, high-throughput processing of UTF-8 encoded strings; the first byte tells you how much to increment by to get the the next codepoint. $\endgroup$ Jul 5, 2023 at 15:44
11
$\begingroup$

The C style doesn't work with parallel programming

Some languages would want to add some built-in support for parallel programming in the basic looping constructs. A C style for doesn't work well in this case. If the language has only C style for, it would surprise the programmer when they want to change something to parallel.

There are cases that a foreach cannot work well, for example in the cases with non-constant increment. But a for loop also doesn't fully replace foreach with parallel programming in consideration.

$\endgroup$
2
  • 1
    $\begingroup$ I think OpenMP would disagree that C-style for-loops are hard to parallelize. Sure, you can write unparallelizable for-loops, but as you mention, you can do the same with foreach-style loops. $\endgroup$
    – G. Sliepen
    Jul 26, 2023 at 15:28
  • $\begingroup$ I would think that even a C-style loop could improve support for parallel programming if a language had a construct to indicate that the compiler may skip whatever subset of the remaining iterations would be most convenient, except that the third expression of the for loop would be run for all iterations prior to the last one executed (and possibly more than that, and that loops avoid having that section modify anything other than automatic objects whose address is not taken. $\endgroup$
    – supercat
    Nov 13, 2023 at 22:55
8
$\begingroup$

A foreach loop is an instance of a for loop, and a for loop is an instance of a while loop. Each is less general than the next.
In particular foreach is specially crafted to loop over items in some collections where both for and while give you control over the looping mechanism.

Here are some examples of for loops which may or may not be translatable to an equivalent foreach:

  • Looping over a constant range:
    for(int x = start; x ⩽ end; x += n) {}
    
  • Looping over a variable range:
    int n = initialStride;
    for(int x = start; x ⩽ end; x += n) {
        n = f(x);
    }
    
  • Looping over an integral's digits:
    for(
        int x = n, digit = x % radix;
        x ≠ 0;
        x = floor(x / radix), digit = x % radix
    ) {}
    
  • Following pointers:
    struct S { struct S* next; }
    for(struct S* p = start; p; p = p->next) {}
    
  • Iterating over a segmented list:
    struct S { struct S* next; size_t length; int elems[length]; };
    assert(start);
    for(
        struct S* sp = start, size_t ep = 0;
        sp && ep < sp->length;
        (++ep == sp->length) ? (sp = sp->next, ep = 0) : 0
    ) {}
    
  • Iterating over multiple lists at once:
    for(
        int (*lp)[] = &xs,
        size_t i = 0,
        elem = (*lp)[0];
        i ≠ END;
        ++i,
        lp = (lp == &xs ? &ys : &xs),
        elem = (*lp)[i]
    ) {}
    
  • Looping with initialisation and incrementation clauses being unrelated to the condition:
    S* p;
    for(xs.prealloc(); p = xs.next(); free(p)) {}
    

In fact when writing macros, it is common to use for's incrementation clause as a way to defer code evaluation.
Here are some examples:

  • lock:
    #define lock(mutex) _lock_impl(UNIQUE_ID(_lock_id_), mutex)
    #define _lock_impl(id, mutex) mtx_lock(mutex); for(char id = 1; id--; mtx_unlock(mutex))
    
    Example use:
    void f()
    {
        static mtx_t mtx = GLBL_MUTEX_INIT;
        lock(mtx) {
            printf("synchronised output\n");
        }
    }
    
  • with resource:
    #define with(init, deinit) _with_impl(UNIQUE_ID(_with_id_), init, deinit)
    #define _with_impl(id, init, deinit) char id = 1; for(init; id--; deinit)
    
    Example use:
    int main()
    {
        with(FILE* f = fopen("./input.txt", "r"), fclose(f)) {
            fseek(f, 0, SEEK_END);
            long int length = ftell(f);
            fseek(f, 0, SEEK_SET);
            with(char* buf = malloc(length), free(buf)) {
                fread(buf, 1, length, f);
                printf("%s", buf);
            }
        }
    }
    

both assuming GNU C's __COUNTER__ and

#define UNIQUE_ID(prefix) CONCAT(prefix, __COUNTER__)
#define CONCAT(x, y) _CONCAT_IMPL(x, y)
#define _CONCAT_IMPL(x, y) x##y
$\endgroup$
7
$\begingroup$

Flexibility is both a pro and a con of C-style for-loops.

You can have a for-loop with an arbitrary setup, arbitrary condition and arbitrary "increment" expressions.

This also means that

for (A; B; C) {
 body;
}

is just syntactic sugar for

{ // Block limits scope of variable declarations in A.
  A;
  while (B) {
    body;
    C;
  }
}

... and depending on the specifics, having that syntactic sugar may not be worth it.

$\endgroup$
3
  • 6
    $\begingroup$ They are kind of equivalent; a continue appearing in the body means something slightly different. $\endgroup$
    – Pseudonym
    May 17, 2023 at 1:15
  • 3
    $\begingroup$ That's true, you'd need to replace any continue; with C; continue; for the equivalency to hold. $\endgroup$
    – Jasmijn
    May 17, 2023 at 8:25
  • $\begingroup$ I think you can wrap the body in a labeled block and rewrite continue to break body_label thus { A; while (B) { body_label: { body; } C; } }. Obviously, body_label needs to be a label chosen to be distinct from any label previously used in body. $\endgroup$ Jul 5, 2023 at 15:57
6
$\begingroup$

Avoiding off-by-one errors

If you're not concentrating, it it really easy to get an off-by-one error with a C-style for loop.

For example, if I want to loop 10 times:

for (i=0; i<=10; i++) {
  // do stuff
}

actually loops 11 times.

But,

for i in range(10):
  # do stuff

is a lot harder to get wrong

$\endgroup$
4
  • 2
    $\begingroup$ Depends on your range syntax; cf. Swift 0...10, 0..<10 $\endgroup$
    – Bbrk24
    May 18, 2023 at 17:01
  • $\begingroup$ @Bbrk24 sure, but I would still say it's harder to get wrong than the C-style for loop. $\endgroup$
    – The Thonnu
    May 18, 2023 at 17:02
  • 2
    $\begingroup$ I tend to add a comment to my for loops which use <= instead of <, it's so rare that I want to make sure readers know it's intentional .... $\endgroup$
    – Glorfindel
    May 18, 2023 at 17:04
  • 1
    $\begingroup$ for (i = start; i < end; ++i) { ... } is such a common pattern in C, that's not "not concentrating", that's "drunk while programming". $\endgroup$
    – gnasher729
    Jul 26, 2023 at 15:30
6
$\begingroup$

One major advantage of a foreach loop is that it makes safe bounds check removals a lot easier for the compiler. A C-style loop requires the compiler to prove a relation between the index and the array while the foreach gets bounds check removal for basically free.

$\endgroup$
2
  • $\begingroup$ This is true for arrays that never reduce in size, but it's not true for mutable, user-defined types that might be changed by the loop body, right? foreach loops based on iterators are only as safe as the iterator implementation in the face of the loop body. For a foreach loop to be safe and support user defined collections you need statically checkable non-mutation, probably either type enforced immutability, or an inability for the body to access the collection via aliases, or some kind of contract that the length doesn't decrease. $\endgroup$ Jul 5, 2023 at 15:42
  • 1
    $\begingroup$ A lot of languages make it undefined behavior to mutate a collection you are foreaching over. $\endgroup$ Jul 5, 2023 at 15:54
4
$\begingroup$

For each is much more ergonomic, declarative, and can even make some optimisations possible like removing bounds checking.

However, compared to C style for loops it’s much less obvious how it compiles to efficient machine code, so in most languages for each is slower than an equivalent C style loop.

The exception is languages with zero cost abstraction, specifically ones with lots of static dispatch and inlining which can compile for each down to the same thing as a C style loop.

$\endgroup$
2
  • 1
    $\begingroup$ I know Swift special-cases Array -- a foreach loop on an array is as efficient as the equivalent C code, but it's impossible to do that for any user-implemented type. $\endgroup$
    – Bbrk24
    May 16, 2023 at 23:15
  • $\begingroup$ Same for Java’s arrays and Kotlin’s ranges $\endgroup$
    – Seggan
    May 16, 2023 at 23:25
4
$\begingroup$

1. C style loop allows joining multiple arrays by index:

for (size_t i = 0; i < a.size(); i++) {
  do_something_together(a[i], b[i]);
}

The use cases are the same as for the join statement in SQL:

select a.x, b.y from a join b on (a.p == b.p)

Here also one may argue, "keep all fields you would ever need together in one table". Unfortunately this does not always work and it may be more important things influencing the clarity of the code. There is often no obvious and easy construct to iterate over multiple arrays, having values at the same position of each array together. See this question for the possible approaches, many of them look complex. This is difficult to add later and should be built into the language, envisioning something like

for (ax: a join bx: b) {
  do_something_together(ax, bx);
}

Of course, to be efficient this should not work just by copying. It should be an assertion that the arrays have the same dimensions or otherwise just "outer join" (loop terminates after reaching the boundary of the shorter array).

I am mostly using the "for each" style loops, but some need to be reworked into "old style" loops if they use two or more arrays this way in the iteration.

2. There are also cases when adjacent elements are needed:

for (size_t i = 1; i < a.size(); i++) {
  double difference = a[i] - a[i-1];
  ...
}

This needs precautions for the first or last member of the array, anyway you also need another member that is not available in the "modern" ranged loop. Algorithms like filtering, smoothing, differentiating and the like often need access to the values that are spread around the current index. Here is the access as required, for the 2D array, by Crank-Nicolson method, the golden classic of numeric computation (image credit):

enter image description here

3. There may be cases when just the number of the iteration is needed, like printing the numbered list. It is very stupid when you cannot easily get it and need to define and maintain a separate counter. The question has been asked here, and, again, most of solutions are just too complex to be practical.

4. Least, not the last: identifying the first and the last iteration. The most obvious case is when all you need is to produce the comma delimited list of items: it must be no comma after the last item. See this question for all fun the people attempted. Surely the classic C++ look also looks ugly, but still:

   for (int k = 0; k < a.size(); k++) {
     if (k + 1 == a.size()) {
        // Last iteration
     }
     ...
   }

something like

for (int x: a) {
  ...
  if (last x) {
     ...
  }      
}

may look nicer, but how can you take value and return something that depends on the position inside the loop. Due that last and first must likely be the keywords built into the language, or some system functions that are processed in a very specific way with the help of compiler.

Some of these problems are also solvable with C++ iterators but the syntax also looks quite cluttered.

P.S. This answer advocates that it is important to have a loop where the loop variable is just an integer, supporting the usual arithmetic and usable with any array. There is a comment that it does not need to be a typical C loop, a simpler syntax like for int x in a .. b would also suffice. But even if loops providing just a reference to the member of collection being iterated look very simple and clean in most cases, there are other cases where they cannot be easily used.

$\endgroup$
4
  • 1
    $\begingroup$ The first one is often mitigated by a zip function, but that doesn't help if you need to modify one or both of the arrays in-place. $\endgroup$
    – Bbrk24
    Jul 5, 2023 at 12:35
  • 1
    $\begingroup$ All of these can more easily be solved by having an iterator over a range of numbers, such as Python's range. $\endgroup$
    – xigoi
    Jul 19, 2023 at 8:03
  • $\begingroup$ Pyton's for x in range(1,5) is very comparable with a C style loop for (int x = 1; x < 5; x++) so of course you can do all the same with it and same about the Swift for i in 1...5 . C style loop is of course originally more complex (assignment, comparison, increment) but a typical C developer recognizes the most common simple case and immediately parses into "ranged" understanding. $\endgroup$ Jul 19, 2023 at 8:07
  • 2
    $\begingroup$ Some languages, say PHP, supports defining both the index and the element in a foreach loop. $\endgroup$
    – user23013
    Jul 26, 2023 at 8:14

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .