Proven C Book한국어 GitHub

41 Loop techniques — nesting, escaping, and making a block

What to know first

chapter 32, Loops and invariants · the three loops while, for, do-while
chapter 39, Multidimensional arrays · rows and columns, and their layout in memory

Looking back

Chapter 32 taught the three loops and chapter 39 the two-dimensional array. So the sum of every cell of int a[1000][1000] can be written two ways — rows first, or columns first. The two give the same answer. What differs?

A. The speed — by about eight times. As chapter 39 showed, a 2-D array is laid out in memory row by row (row-major), and as chapter 11 showed, the cache brings in neighbouring bytes together. Walk along a row and you spend the line you fetched; walk down a column and every step calls for a new one.

Having learned loops and knowing how to drive them are different things. This chapter fills that gap.

The need for this chapter, and its context

Chapter 32 taught the syntax of loops, but the practical techniques only mean something after multidimensional arrays — nested loops and traversal order cannot be exemplified without chapter 39. So repetition is split into two chapters, one in part 6 and one in part 7. It is the most widely separated pair in this book.

By the end of this chapter

The practical techniques of loops, gathered — nesting and traversal order (measured), the four ways of leaving nested loops and how they rank, the two places where goto is right and the discipline it demands, the do { } while (0) that makes a macro one statement, and the idioms and traps met over and over in practice. If chapter 32 was “what a loop is”, this chapter is “how a loop is driven”.

The questions this chapter answers

  1. So should the last subscript always go innermost?
  2. What about conventions (MISRA and the like) that forbid goto outright?
  3. while (0) — doesn’t that mean it never runs?
  4. In a do-while, where does continue go?

41.1 Nested loops — walking a multidimensional array

Chapter 39′s int a[R][C] lies in memory as one line, in row-major order — a[0][0] a[0][1] … a[0][C-1] a[1][0] …. The order of the nested loops decides with what stride you walk that line.

for (int i = 0; i < R; i++)          /* rows first — one step to the neighbour */
    for (int j = 0; j < C; j++)
        sum += a[i][j];

for (int j = 0; j < C; j++)          /* columns first — a whole row skipped each step */
    for (int i = 0; i < R; i++)
        sum += a[i][j];

The same computation. Measured, not the same at all.

TraversalSumming 4000×4000 intWhy
rows first (i, j)11–14 msit uses up the line the cache fetched, then moves on
columns first (j, i)101–113 mseach step jumps 16 KB and calls for a new line

Table 46.1

Eight times. Same algorithm, same operation count, same result. Chapter 11′s “the cache governs speed” becomes a visible number here.

So practice’s first optimisation is usually changing the loop order — without complicating the code or touching the algorithm, just swapping two lines so that the inner subscript is the axis that varies fastest.

Q. So should the last subscript always go innermost?

A. Usually, yes. Two things go with it though.

1. The layout decides what is right. C is row-major, but Fortran is column-major, so numerical code ported from Fortran may be right the other way round. If it is not an array but an array of structs, it changes again (chapter 46′s layout story).

2. Some computations cannot have their order changed. Where the previous cell’s result feeds the next (accumulation, recurrences), the order is part of the algorithm. What is used then is tiling — cutting the work into blocks that fit the cache — which is beyond this book.

One rule is worth keeping: let the innermost loop move most tightly through memory. That is a factor of several, for free.

41.2 Leaving nested loops

Write a nested loop and you hit a problem at once.

A common misconception. break leaves the loop”

It leaves the innermost layer only. The standard says so — break ends the nearest enclosing switch or iteration statement, one of them.

The listing measured it. Code that broke only in the inner loop visited 18 of the table’s 20 cells — it found the value and the outer loop kept going.

And C has no labelled break. Here it parts from other languages.

LanguageDevice for shedding several layers at once
Cnone — goto or another way
Javaouter: for (…) { break outer; }
Gothe same label syntax; continue takes one too
Rust'outer: loop { break 'outer; } — it even returns a value
Perl, PHPlast LABEL / break 2 (the depth as a number)

Table 46.2

C offers four ways, each worth something different.

examples-en/ch41/nested_exit.c

/* Three ways to leave nested loops after finding a value in a 2-D table.
   All three give the same answer; what differs is how easily a reader
   follows the flow. */
#include <stdbool.h>
#include <stdio.h>

#define ROWS 4
#define COLS 5

static const int grid[ROWS][COLS] = {
    {  3, 14, 15,  92,  6 },
    { 53, 58,  9,  79,  3 },
    { 23, 84, 62,  64, 33 },
    { 83, 27, 95,  28, 84 },
};

struct pos { int row, col; bool found; };

/* -- 1. a flag variable --
   The flag rides on the outer loop's condition. Plain standard C, but the
   condition grows and "where it ended" is scattered over two places. */
static struct pos find_flag(int target)
{
    struct pos p = { -1, -1, false };
    for (int i = 0; i < ROWS && !p.found; i++)
        for (int j = 0; j < COLS; j++)
            if (grid[i][j] == target) { p.row = i; p.col = j; p.found = true; break; }
    return p;
}

/* -- 2. goto --
   "shed both layers at once" shows in a single line.
   The label goes below, and is named for what it does. */
static struct pos find_goto(int target)
{
    struct pos p = { -1, -1, false };
    for (int i = 0; i < ROWS; i++)
        for (int j = 0; j < COLS; j++)
            if (grid[i][j] == target) { p.row = i; p.col = j; p.found = true; goto done; }
done:
    return p;
}

/* -- 3. lift it into a function and return --
   return sheds any number of layers at once; no escape device is needed. */
static struct pos find_return(int target)
{
    for (int i = 0; i < ROWS; i++)
        for (int j = 0; j < COLS; j++)
            if (grid[i][j] == target) return (struct pos){ i, j, true };
    return (struct pos){ -1, -1, false };
}

static void show(const char *how, struct pos p)
{
    if (p.found) printf("  %-18s (%d, %d)\n", how, p.row, p.col);
    else         printf("  %-18s not found\n", how);
}

int main(void)
{
    puts("[looking for 62 — the three ways agree]");
    show("1. flag", find_flag(62));
    show("2. goto", find_goto(62));
    show("3. function+return", find_return(62));

    puts("\n[looking for a value that is not there (100)]");
    show("1. flag", find_flag(100));
    show("2. goto", find_goto(100));
    show("3. function+return", find_return(100));

    puts("\n[break sheds one layer only — hence the devices above]");
    int visited = 0;
    for (int i = 0; i < ROWS; i++)
        for (int j = 0; j < COLS; j++) { visited++; if (grid[i][j] == 62) break; }
    printf("  break in the inner loop only: visited %d cells (of %d)\n",
           visited, ROWS * COLS);
    puts("  -> only the inner loop ended; the outer one kept going.");
    return 0;
}

Output

[looking for 62 — the three ways agree]
  1. flag            (2, 2)
  2. goto            (2, 2)
  3. function+return (2, 2)

[looking for a value that is not there (100)]
  1. flag            not found
  2. goto            not found
  3. function+return not found

[break sheds one layer only — hence the devices above]
  break in the inner loop only: visited 18 cells (of 20)
  -> only the inner loop ended; the outer one kept going.
WayHowGoodBad
  1. a flag variable
put && !found on the outer conditionno gotothe condition grows, and where it ends is scattered over two places. One more test per iteration too
  1. goto
jump to a label the moment it is foundit says “shed both layers at once” in one linesome conventions forbid goto
  1. a function and return
make the search a functionreturn sheds any depth. No escape device at allone more function, and returning several values needs a struct
  1. condition on the outer loop
for (i = 0; i < R && !done; i++)a variant of 1the same problem as 1

Table 46.3

This book’s recommendation is 3 → 2 → 1.

Three comes first because the problem is not the escape but the lump. If a nested loop has grown long enough that you are contriving a way out, its inside has already become “something worth naming”. Lift it into a function and it gains a name, return solves the escape for free, and it becomes testable.

Two beats one because the intent shows. A flag makes the reader reconstruct “when this variable becomes true, the loop ends somewhere”; goto done; says right there that it ends. And a flag is easy to get wrong — forget the inner break and it quietly runs one more round.

41.3 The two places goto is right

Clear away the misunderstanding first.

In practice. What Dijkstra actually objected to

Dijkstra’s 1968 letter to CACM, “Go To Statement Considered Harmful”, may be the most cited and least read piece in programming’s history.

Its argument is not “do not use the word goto”. It is that one must be able to map a point in the program’s text onto the progress of its execution, and that unrestrained jumping destroys that mapping. Even the title’s “considered harmful” is understood to have come not from Dijkstra but from the editor, Niklaus Wirth.

Seen from today, most of what that letter demanded has already happenedwhile, for, functions, break and return replaced nearly every use of the goto of that era. What remains are the two places below, and in them goto makes the flow easier to read. That is why the Linux kernel’s coding style explicitly permits it.

41.3.1 Place 1 — shedding several loops at once

The pattern from the previous section. The rules are downwards only, and nearby.

for (int i = 0; i < R; i++)
    for (int j = 0; j < C; j++)
        if (grid[i][j] == target) { found = (struct pos){ i, j }; goto done; }
done:

41.3.2 Place 2 — gathering the cleanup of error handling

A function that acquires several resources must give back only what it acquired so far when it fails midway. Written with ifs, the giving-back code gets copied layer upon layer.

examples-en/ch41/macro_block.c

/* do { } while (0), which makes a macro one statement — and cleanup by goto. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* With braces alone, the trailing semicolon makes it two statements,
   so putting it between if and else is a compile error:

       #define SWAP_BAD(a, b) { int t = (a); (a) = (b); (b) = t; }
       if (x < y) SWAP_BAD(x, y); else puts("...");
           → error: 'else' without a previous 'if'

   do { } while (0) makes it a braced block and a single statement at once. */
#define SWAP(a, b)  do { int t_ = (a); (a) = (b); (b) = t_; } while (0)

#define LOG(fmt, ...)  do {                       \
        printf("[log] " fmt "\n", __VA_ARGS__);   \
    } while (0)

/* Gathering the cleanup in one place with goto — the Linux kernel's shape.
   Fail midway and only what was acquired so far is given back. */
static bool build(size_t n, bool fail_at_second)
{
    char *a = nullptr, *b = nullptr;
    bool ok = false;

    a = malloc(n);
    if (!a) goto out;                 /* nothing to give back yet */
    memset(a, 'a', n);

    b = fail_at_second ? nullptr : malloc(n);
    if (!b) goto free_a;              /* give back only a */
    memset(b, 'b', n);

    ok = true;

    free(b);
free_a:
    free(a);
out:
    return ok;
}

int main(void)
{
    int x = 1, y = 2;

    puts("[do { } while (0) — the macro becomes one statement]");
    if (x < y) SWAP(x, y); else puts("  (we do not come here)");
    printf("  after the swap: x=%d y=%d   <- unbroken between if and else\n", x, y);

    LOG("values %d and %d", x, y);

    puts("\n[safe as a for body without braces, too]");
    for (int i = 0; i < 2; i++)
        LOG("iteration %d", i);

    puts("\n[gathering cleanup with goto]");
    printf("  both succeed: %s\n", build(16, false) ? "ok" : "failed");
    printf("  second one fails: %s  <- only the first was given back\n",
           build(16, true) ? "ok" : "failed");
    return 0;
}

Output

[do { } while (0) — the macro becomes one statement]
  after the swap: x=2 y=1   <- unbroken between if and else
[log] values 2 and 1

[safe as a for body without braces, too]
[log] iteration 0
[log] iteration 1

[gathering cleanup with goto]
  both succeed: ok
  second one fails: failed  <- only the first was given back

The listing’s build is that pattern. The labels are stacked in reverse order of release, and a failure jumps to the label that matches it.

    a = malloc(n);
    if (!a) goto out;          /* nothing to give back yet */
    b = malloc(n);
    if (!b) goto free_a;       /* give back only a */free(b);
free_a:
    free(a);
out:
    return ok;

There is exactly one copy of the cleanup code — that is what the pattern buys. Add a third or fourth resource and only a label is added; nothing is duplicated. It is how chapter 44′s pairing of malloc and free is kept in practice.

The discipline for gotoWhy
jump downwards onlya jump upwards is a loop, and a loop should be written with loop syntax to be read
jump nearby — same function, within sightjump far and Dijkstra’s problem really does appear
name labels for what they dodone, cleanup, free_buflabel1 is not a name
do not skip an initialisationa variable whose initialisation was skipped holds an indeterminate value, and jumping into the scope of a variable-length array is a constraint violation the compiler diagnoses

Table 46.4

Q. What about conventions (MISRA and the like) that forbid goto outright?

A. As chapter 96 will show, rulebooks such as MISRA forbid or tightly limit goto. Three alternatives are used there.

  • Lift it into a function. Number 3 above. Much of the cleanup problem also dissolves once the work is split into small “acquire, use, release” functions.
  • A do { … } while (0) run once. A break inside it gives the same effect as “jump to the cleanup” — the next section’s pattern used for control flow.
  • One state variable stepping through stages. Chaining if (ok) { … }, which gets harder to read as the stages multiply.

Judge it by chapter 12′s ladder — where a rulebook governs, follow the rulebook and pay its price (longer code) knowingly. Where none governs, there is no reason to bind yourself with “goto is always bad”.

41.4 Making a macro one statement — do { } while (0)

A macro holding several statements has an old trap. Wrapping it in braces is not enough.

#define SWAP_BAD(a, b)  { int t = (a); (a) = (b); (b) = t; }

if (x < y) SWAP_BAD(x, y); else puts("...");

Expand it and the reason shows — it becomes if (x < y) { … }; else …, where the semicolon after the braces is an empty statement. The if ends on that empty statement and the else loses its partner. Measured, GCC says:

error: 'else' without a previous 'if'

do { } while (0) solves exactly this, because it is a braced block and, at the same time, a single statement that wants a semicolon after it.

Macro bodybetween if and elseas a for body (no braces)the semicolon
bare statementsbreaksonly the first statement repeats — a silent accidentnot needed (confusing)
{ … }breaksfineadding one creates an empty statement
do { … } while (0)finefinenaturally required

Table 46.5

In the listing, SWAP and LOG work between if and else and as a brace-less for body alike — that is the confirmation.

Q. while (0) — doesn’t that mean it never runs?

A. It is a do-while, so it runs the body first and tests afterwards (chapter 32). The condition being false, it runs exactly once — which is the point.

Compilers know the pattern too. Even built without optimisation it folds into “a loop whose condition is always false”, so the run-time cost is zero. It merely borrows the shape of a loop; no loop actually loops.

Counter-example. Using do { } while (0) for a macro that yields a value

#define MAX(a, b)  do {} while (0)   /* cannot yield a value */
int m = MAX(x, y);                       /* a statement cannot be assigned */

do { } while (0) is a tool for making statements. When a value is needed, pick one of three.

  • An expression macro#define MAX(a, b) ((a) > (b) ? (a) : (b)). Parenthesise everything, and accept that an argument is evaluated twice (which is why MAX(i++, j) is an accident).
  • A static inline function — it has types and evaluates each argument once. Since C99 this is the proper answer (chapter 24).
  • _Generic choosing a function per type — when several types must be taken (chapters 26 and 57).

GCC and Clang have a statement-expression extension ({ … }) that does both, but it is not standard (chapter 12′s grey area). Where portability matters, use the three above.

In practice. do { } while (0) in the standard library and the kernel

The pattern is not a habit but a de facto standard. Open the Linux kernel’s headers and do { } while (0) appears thousands of times; the coding-style document requires the form for multi-statement macros.

There is an amusing variant. A macro that does nothing — a log macro outside a debug build, say — if defined as an empty #define LOG(...) leaves a bare semicolon, which breaks places like if (x) LOG(…);. So it is defined as #define LOG(...) do { } while (0): an empty do-while, doing nothing while keeping the property of being a statement.

As a bonus, unused macro arguments can raise “unused variable” warnings, so the habit of writing do { (void)(x); } while (0) settled in alongside it.

41.5 Other idioms, and their traps

examples-en/ch41/loop_idioms.c

/* A few loop idioms — and the places that bite quietly. */
#include <stddef.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
    int a[] = { 10, 20, 30, 40, 50 };
    size_t n = sizeof a / sizeof a[0];

    puts("[walking backwards — with size_t you cannot write i >= 0]");
    printf("  the right shape (i-- > 0): ");
    for (size_t i = n; i-- > 0; ) printf("%d ", a[i]);
    puts("");
    puts("  the wrong shape (size_t i = n-1; i >= 0; i--) loops forever —");
    puts("  after 0 an unsigned value wraps to SIZE_MAX, so the test never fails.");
    printf("  with a signed index that shape works too: ");
    for (int i = (int)n - 1; i >= 0; i--) printf("%d ", a[i]);
    puts("");

    puts("\n[the comma operator — closing in from both ends]");
    printf("  reversed: ");
    int b[] = { 1, 2, 3, 4, 5, 6 };
    size_t m = sizeof b / sizeof b[0];
    for (size_t i = 0, j = m - 1; i < j; i++, j--) {
        int t = b[i]; b[i] = b[j]; b[j] = t;
    }
    for (size_t i = 0; i < m; i++) printf("%d ", b[i]);
    puts("");

    puts("\n[a sentinel loop — read and test in one breath]");
    const char *text = "hi!\n";
    const char *p = text;
    int c, count = 0;
    while ((c = *p++) != '\0') {          /* the parentheses are required */
        if (c == '\n') printf("\\n ");
        else printf("%c ", c);
        count++;
    }
    printf("\n  read %d characters\n", count);

    puts("\n[do not call strlen in the loop condition]");
    const char *s = "measure once";
    size_t len = strlen(s);               /* measured once */
    size_t vowels = 0;
    for (size_t i = 0; i < len; i++)
        if (strchr("aeiou", s[i]) && s[i]) vowels++;
    printf("  \"%s\" has %zu vowels (length %zu, measured once)\n", s, vowels, len);

    puts("\n[write the intent when a body is empty]");
    size_t skip = 0;
    const char *q = "   value";
    while (q[skip] == ' ')
        skip++;                            /* written with a body */
    printf("  skipped %zu leading spaces\n", skip);
    puts("  while (q[skip++] == ' ') ;  is shorter, but that lone");
    puts("  semicolon goes unnoticed, and that is where accidents come from.");
    return 0;
}

Output

[walking backwards — with size_t you cannot write i >= 0]
  the right shape (i-- > 0): 50 40 30 20 10 
  the wrong shape (size_t i = n-1; i >= 0; i--) loops forever —
  after 0 an unsigned value wraps to SIZE_MAX, so the test never fails.
  with a signed index that shape works too: 50 40 30 20 10 

[the comma operator — closing in from both ends]
  reversed: 6 5 4 3 2 1 

[a sentinel loop — read and test in one breath]
h i ! \n 
  read 4 characters

[do not call strlen in the loop condition]
  "measure once" has 6 vowels (length 12, measured once)

[write the intent when a body is empty]
  skipped 3 leading spaces
  while (q[skip++] == ' ') ;  is shorter, but that lone
  semicolon goes unnoticed, and that is where accidents come from.

41.5.1 Walking backwards — i >= 0 is unusable with size_t

The most frequent bite.

for (size_t i = n - 1; i >= 0; i--)   /* an infinite loop */

An unsigned value is always at least 0, so the condition never fails, and i-- at 0 wraps to SIZE_MAX (chapter 27). GCC says so through -Wtype-limits (included in -Wall -Wextra): “comparison of unsigned expression in >= 0 is always true”.

There are two right shapes.

for (size_t i = n; i-- > 0; )   /* the idiom — the test does the decrement too */
for (int i = (int)n - 1; i >= 0; i--)   /* a signed index */

The first is the common one. i-- > 0 means “see whether the current value exceeds 0, then subtract one” (chapter 49′s postfix), so the body sees n-1 down to 0 in turn.

41.5.2 The rest of the idioms

IdiomWhat it isWatch out
for (;;) / while (1)an infinite loop; the two are identicalGCC warns about neither (measured). Many codebases prefer for (;;) because MSVC used to warn on while (1)
the comma operatorfor (i = 0, j = m - 1; i < j; i++, j--)closing in from both ends. Outside this place the convention is not to use the comma operator
a sentinel loopwhile ((c = getchar()) != EOF)the parentheses are required — without them it is c = (getchar() != EOF). And c must be an int (chapter 64)
strlen in the loop conditionfor (i = 0; i < strlen(s); i++)it counts the string from the start every round. Take the length into a variable (chapter 42)
a floating-point counterfor (double x = 0; x != 1.0; x += 0.1)it may never end — 0.1 is not exact (chapter 50). Count in integers and divide
an empty body ;while (*p++) ;short, but the semicolon is invisible. See below

Table 46.6

Counter-example. An empty body nobody notices

for (int i = 0; i < n; i++);      /* one semicolon and the body is gone */
    total += a[i];                /* outside the loop — and i is not here */

The indentation promises repetition; what happens is that the loop spins empty and ends.

★Measured, the warnings do not help. -Wempty-body catches if (x); with “suggest braces around empty body in an ‘if’ statement” but does not catch for (…); or while (…); — those are a deliberate idiom.

So discipline has to stop it — if an empty body is the intent, make it visible.

while (*p++ != '\0')
    continue;                     /* "this is empty on purpose" */

Q. In a do-while, where does continue go?

A. To the test — not to the top of the body. It is the same rule as in while and for (in a for it passes through the update expression first).

It confuses people because a do-while’s condition is written below, giving the impression that continue “goes back up”. The actual flow is “down to the test, and back up if it is true”. Measured, the output matches the same code written as a while.

We have nesting, escaping, and the making of a statement out of a macro. But this chapter’s listings kept walking char arrays — and the next chapter is the agreement attached to such an array: the string.