C / BRANCHING AND SWITCH
Nested conditionals and guard clauses that flatten them
Flatten a nested if pyramid in C into a flat run of guard clauses using early return, continue, or goto cleanup, and know when nesting is the honest shape.
What you will learn
- Rewrite a four-deep if pyramid as four one-line early returns plus the real work
- Use continue as a guard so a loop body stays at one indentation level
- Order guards so the NULL check runs before anything dereferences the pointer
- Route a guard that fires after malloc through one goto cleanup exit, not a bare return
Understanding Nested conditionals and guard clauses that flatten them
A nested conditional asks the reader to hold a stack of open facts in their head. By the time you reach a->balance -= amount inside withdraw_nested below, four conditions are still open, and the four else branches that handle the failures are piled at the bottom in reverse order, each one separated from the if it answers. The line that does the actual work ends up being the most indented line in the function, which is backwards: the important code is the hardest to find.
A guard clause inverts each test. Instead of if (ok) { rest of function }, you write if (!ok) return code; and the rest of the function continues at the outer level. The mental model is a corridor of gates: once control passes a gate, that gate's guarantee holds for every line below it, so guarantees accumulate downward and later code never re-tests them. That is why the guarded version needs no else at all, since reaching the next line already means the previous check passed.
In C a guard needs somewhere to go. Inside a function that is return; inside a loop it is continue to skip one item or break to stop; and inside a function that already owns a resource it must be goto cleanup, because a bare return below a malloc or fopen leaks it. Guard order is not cosmetic either: a guard may only use facts the guards above it established, which is why the pointer test must precede any dereference. Guards pay off when rejection is short, so when two paths both do real work, keeping the structure nested or exclusive describes the code more honestly than a fake early exit.
<stdio.h>
enum { OK = 0, ERR_NULL = 1, ERR_FROZEN = 2, ERR_AMOUNT = 3, ERR_FUNDS = 4 };
typedef struct {
int balance;
int frozen;
} Account;
/* Nested: the one line that matters is four levels deep, and every else
sits far below the if it belongs to. */
int withdraw_nested(Account *a, int amount)
{
if (a != NULL) {
if (!a->frozen) {
if (amount > 0) {
if (amount <= a->balance) {
a->balance -= amount;
return OK;
} else {
return ERR_FUNDS;
}
} else {
return ERR_AMOUNT;
}
} else {
return ERR_FROZEN;
}
} else {
return ERR_NULL;
}
}
/* Guarded: reject each bad case on the way in. Every line below the guards
may assume a is valid, not frozen, and able to cover the amount. */
int withdraw_guarded(Account *a, int amount)
{
if (a == NULL) return ERR_NULL;
if (a->frozen) return ERR_FROZEN;
if (amount <= 0) return ERR_AMOUNT;
if (amount > a->balance) return ERR_FUNDS;
a->balance -= amount;
return OK;
}
int main(void)
{
Account acct = { 100, 0 };
Account cold = { 50, 1 };
int rc;
rc = withdraw_nested(&acct, 30);
printf("nested rc=%d balance=%d\n", rc, acct.balance);
rc = withdraw_guarded(&acct, 30);
printf("guarded rc=%d balance=%d\n", rc, acct.balance);
rc = withdraw_guarded(&cold, 10);
printf("frozen rc=%d balance=%d\n", rc, cold.balance);
rc = withdraw_guarded(&acct, -5);
printf("negative rc=%d balance=%d\n", rc, acct.balance);
rc = withdraw_guarded(&acct, 999);
printf("overdraw rc=%d balance=%d\n", rc, acct.balance);
printf("null rc=%d\n", withdraw_guarded(NULL, 10));
return 0;
}
A guard clause replaces a wrapping if with an immediate exit, so each check leaves behind a fact the rest of the function can assume instead of another level of indentation.
Worked examples
continue as a guard inside a loop
Three continue guards replace one triple-&& condition that would otherwise wrap the whole loop body.
<stdio.h>
int main(void)
{
int data[] = { 4, -7, 0, 15, 3, 22, -1, 10, 8 };
int n = (int)(sizeof data / sizeof data[0]);
int i, sum = 0, used = 0;
for (i = 0; i < n; i++) {
if (data[i] <= 0) continue; /* not a real reading */
if (data[i] % 2 != 0) continue; /* odd values are discarded */
if (data[i] > 20) continue; /* out of calibrated range */
sum += data[i];
used++;
printf("kept %d\n", data[i]);
}
printf("used=%d sum=%d\n", used, sum);
return 0;
}
Example explained
Line 1if (data[i] <= 0) continue; ends this iteration, so every line under it may assume data[i] is positive.
Line 222 passes the first two guards and is rejected only by the third, so its printf never runs.
Line 3continue in a for loop still executes i++ from the loop header, which is what makes this rewrite safe here.
Line 4The kept-value body stays at one indentation level, so a fourth rule costs one line rather than another brace level.
Guards that fire after allocation need one exit
Guards placed before malloc can return directly, while a guard below it must jump to a single cleanup label.
<stdio.h>
<stdlib.h>
<string.h>
/* 0 on success, negative code on rejection. */
static int copy_upto(const char *src, size_t limit, char **out)
{
char *buf;
size_t len;
int rc = 0;
if (src == NULL) return -1;
if (out == NULL) return -2;
if (limit == 0) return -3;
len = strlen(src);
buf = malloc(limit + 1);
if (buf == NULL) return -4;
if (len >= limit) { rc = -5; goto done; }
memcpy(buf, src, len + 1);
*out = buf;
return 0;
done:
free(buf);
return rc;
}
int main(void)
{
char *p = NULL;
int rc;
rc = copy_upto("hello", 8, &p);
printf("rc=%d p=%s\n", rc, p);
free(p);
p = NULL;
rc = copy_upto("hello world", 8, &p);
printf("rc=%d\n", rc);
rc = copy_upto(NULL, 8, &p);
printf("rc=%d\n", rc);
return 0;
}
Example explained
Line 1The first three guards run before malloc, so nothing is owned yet and a plain return is correct.
Line 2The len >= limit guard runs after buf exists, so it records rc and jumps instead of returning.
Line 3done: free(buf); return rc; is the one exit shared by every failure that happens after allocation.
Line 4The success path returns above the label, which is why buf is not freed when ownership passes to the caller.
Important notes
Guard order encodes dependency, not taste: swapping the first two guards of withdraw_guarded makes withdraw_guarded(NULL, 10) read through a null pointer, which is undefined behaviour instead of ERR_NULL.
Some C style rules restrict a function to a single return; you can still flatten the pyramid by having each guard set a status variable and goto one exit label.
Common mistakes
Negating a && b as !a && !b instead of !a || !b: an input that fails only one of the two tests walks straight past the guard and the body runs on invalid data.
Adding an early return below a malloc or fopen: every rejected input leaks a buffer or file handle while the function still returns the right code, so tests pass and memory grows.
Using continue as a guard in a while loop whose counter is incremented at the bottom of the body: the increment is skipped and the program spins forever on the first rejected item.
Try it yourself
Change, predict, then run
Write int login_check(const char *user, int attempts) as a three-deep nested if that returns 1 for a NULL user, 2 when strlen(user) is outside 3 to 12, 3 when attempts >= 5, and 0 otherwise. Then write a guarded version and print both results for ("", 0), ("ada", 9), ("ada", 1) and (NULL, 0) to prove they agree.
Open the C workspaceCheck your understanding
withdraw_guarded works because each guard may assume every guard above it already passed. Which single change turns a defined error return into undefined behaviour?
- Moving if (amount > a->balance) return ERR_FUNDS; above if (amount <= 0) return ERR_AMOUNT;
- Moving if (a->frozen) return ERR_FROZEN; above if (a == NULL) return ERR_NULL;
- Rewriting if (amount <= 0) as if (!(amount > 0))
- Merging the last two guards into if (amount <= 0 || amount > a->balance) return ERR_AMOUNT;
Show answer
The frozen test reads through a, so running it first makes withdraw_guarded(NULL, 10) dereference a null pointer, which is undefined behaviour rather than ERR_NULL; the NULL guard exists precisely to establish the fact the next line depends on. Option 0 is tempting because reordering sounds equally unsafe, but those two tests are independent and for the non-negative balances this code maintains no amount can fail both, so nothing observable changes and nothing can crash. Option 3 only coarsens the reported code, and option 2 is the same test written differently.