41 Loop techniques — nesting, escaping, and making a block
What to know first
while, for, do-whileLooking 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
By the end of this chapter
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
- So should the last subscript always go innermost?
- What about conventions (MISRA and the like) that forbid
gotooutright? while (0)— doesn’t that mean it never runs?- In a
do-while, where doescontinuego?
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.
| Traversal | Summing 4000×4000 int | Why |
|---|---|---|
rows first (i, j) | 11–14 ms | it uses up the line the cache fetched, then moves on |
columns first (j, i) | 101–113 ms | each 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.
| Language | Device for shedding several layers at once |
|---|---|
| C | none — goto or another way |
| Java | outer: for (…) { break outer; } |
| Go | the same label syntax; continue takes one too |
| Rust | 'outer: loop { break 'outer; } — it even returns a value |
| Perl, PHP | last 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.
| Way | How | Good | Bad |
|---|---|---|---|
| put && !found on the outer condition | no goto | the condition grows, and where it ends is scattered over two places. One more test per iteration too |
| jump to a label the moment it is found | it says “shed both layers at once” in one line | some conventions forbid goto |
| make the search a function | return sheds any depth. No escape device at all | one more function, and returning several values needs a struct |
| for (i = 0; i < R && !done; i++) | a variant of 1 | the 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 happened — while, 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 goto | Why |
|---|---|
| jump downwards only | a jump upwards is a loop, and a loop should be written with loop syntax to be read |
| jump nearby — same function, within sight | jump far and Dijkstra’s problem really does appear |
| name labels for what they do | done, cleanup, free_buf — label1 is not a name |
| do not skip an initialisation | a 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. Abreakinside 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 body | between if and else | as a for body (no braces) | the semicolon |
|---|---|---|---|
| bare statements | breaks | only the first statement repeats — a silent accident | not needed (confusing) |
{ … } | breaks | fine | adding one creates an empty statement |
do { … } while (0) | fine | fine | naturally 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 whyMAX(i++, j)is an accident). - A
static inlinefunction — it has types and evaluates each argument once. Since C99 this is the proper answer (chapter 24). _Genericchoosing 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
| Idiom | What it is | Watch out |
|---|---|---|
for (;;) / while (1) | an infinite loop; the two are identical | GCC warns about neither (measured). Many codebases prefer for (;;) because MSVC used to warn on while (1) |
| the comma operator | for (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 loop | while ((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 condition | for (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 counter | for (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.