C / BRANCHING AND SWITCH
The ternary operator and when it hurts readability
Use C's conditional operator where the code needs a value, predict its result type and precedence, and recognise when an if/else reads better.
What you will learn
- Reach for ?: only where C needs a value: an initializer, an argument, a return.
- Predict a ?: result type from both arms, since conversions happen before selection.
- Parenthesize ?: inside larger expressions and macro bodies; its precedence is low.
- Rewrite as if/else when an arm has side effects or nests inside the true arm.
Understanding The ternary operator and when it hurts readability
if is a statement: it decides which code runs. ?: is an operator: it decides what value an expression has. That single difference is the design rule — use ?: when the surrounding code demands one value (an initializer, a function argument, the expression after return) and use if when you want two different actions. Written on its own line, valid ? do_a() : do_b(); does compile, but it is an expression impersonating a branch, and every reader has to stop and re-parse it.
Three mechanics explain most surprises. Exactly one arm is evaluated, and there is a sequence point after the condition, so p ? p->len : 0 is safe and c ? i++ : i-- is well defined. The type of the whole expression, though, is computed from both arms by the usual arithmetic conversions before anything runs, so 1 ? 2 : 3.0 has type double even though the double arm is never evaluated. And ?: sits near the bottom of the precedence table, just above assignment and comma, so almost any neighbouring operator binds tighter than it does.
A ternary reads well when the condition is short, the arms have the same type, and the whole thing fits on the line where the value is consumed: n == 1 ? "" : "s" as a printf argument beats four lines of if/else writing to a temporary. It reads badly when an arm hides a side effect, when one nests inside the true arm, when it wraps across lines, or when the reader must recall precedence to find where the arms end. A practical signal: if you need extra parentheses or column alignment before a human can parse the expression, that branch wanted to be an if.
Chained conditionals in the false arm are the one deep form that survives review, because right associativity makes them read top-to-bottom like else-if; everything else that grows past one colon is better off flat.
<stdio.h>
/* Fine: one small value depends on one condition. */
static const char *plural(int n)
{
return n == 1 ? "" : "s";
}
/* Dense: three decisions and a nested ternary in one expression. */
static int fee_dense(int age, int member)
{
return age < 12 ? 0 : age < 65 ? (member ? 5 : 8) : member ? 3 : 6;
}
/* Same rule, one readable line per case. */
static int fee_plain(int age, int member)
{
if (age < 12) return 0;
if (age >= 65) return member ? 3 : 6;
return member ? 5 : 8;
}
int main(void)
{
int ages[] = { 8, 30, 30, 70 };
int members[] = { 0, 0, 1, 1 };
int n = (int)(sizeof ages / sizeof ages[0]);
int i;
printf("%d visitor%s\n", n, plural(n));
for (i = 0; i < n; i++)
printf("age %2d member %d -> %d %d\n", ages[i], members[i],
fee_dense(ages[i], members[i]),
fee_plain(ages[i], members[i]));
return 0;
}
?: chooses a value rather than an action, and its type is fixed by both arms before either arm runs, which is why it belongs inside expressions and if/else belongs everywhere else.
Worked examples
The MAX macro trap
Shows how low precedence and a repeated argument turn the classic ternary macro into two silent bugs.
<stdio.h>
MAX_BAD(a, b)
MAX_OK(a, b)
int main(void)
{
int i = 5, j = 2;
printf("%d\n", MAX_BAD(3, 1) * 2);
printf("%d\n", MAX_OK(3, 1) * 2);
printf("%d\n", MAX_OK(i++, j));
printf("i = %d\n", i);
return 0;
}
Example explained
Line 1MAX_BAD(3, 1) * 2 expands to (3) > (1) ? (3) : (1) * 2, so * 2 attaches to the false arm only and the result is 3.
Line 2MAX_OK wraps the conditional in parentheses, so * 2 multiplies the selected value and gives 6.
Line 3MAX_OK(i++, j) mentions i++ twice, once in the condition and once in the true arm, so i ends at 7 and the macro yields the second increment's value, 6.
Line 4The sequence point after the condition makes that double increment defined behaviour rather than a crash, which is exactly why it survives testing.
The result type is decided by both arms
Demonstrates that the conditional expression has one common type even though only one arm is evaluated.
<stdio.h>
int main(void)
{
int flag = 1;
int i = 3;
double d = 4.5;
printf("%f\n", flag ? i : d);
printf("%f\n", !flag ? i : d);
if (flag) printf("%d\n", i);
else printf("%f\n", d);
return 0;
}
Example explained
Line 1flag ? i : d has type double because the usual arithmetic conversions are applied to both arms, so the int 3 arrives as 3.000000.
Line 2Writing printf("%d\n", flag ? i : d) instead would pass a double where %d expects an int: undefined behaviour, not a rounding difference.
Line 3!flag ? i : d selects d and prints 4.500000, confirming the choice of arm still happens at run time.
Line 4The if/else version keeps the two types in separate calls, so each conversion specifier matches its own argument.
Assigning through a conditional
Shows that ?: never produces an lvalue in C and the pointer idiom people use instead.
<stdio.h>
int main(void)
{
int a = 1, b = 2;
int pick_a = 0;
/* (pick_a ? a : b) = 99; rejected by the compiler in C */
*(pick_a ? &a : &b) = 99;
printf("a = %d, b = %d\n", a, b);
return 0;
}
Example explained
Line 1In C the conditional operator yields a value, not an object, so it can never sit on the left of =; C++ differs here, which is a common source of confusion.
Line 2Choosing between &a and &b makes a pointer the value, and the dereference performs the assignment on the selected object.
Line 3pick_a is 0, so &b wins and only b changes.
Line 4It works, but two plain assignments under if/else say the same thing without asking the reader to follow a pointer.
Important notes
The result of ?: is never an lvalue in C, so (c ? a : b) = 0; is a compile error; select pointers and assign through them, or just use if/else.
Chaining in the false arm (a ? 1 : b ? 2 : 3) stays readable because ?: is right associative; nesting in the true arm (a ? b ? 1 : 2 : 3) is the shape that stops reading like a sentence.
Common mistakes
Putting statements in an arm: cond ? return 1 : return 0; and an empty arm like cond ? puts("hi") : ; fail to compile, because both arms must be expressions.
Mixing arm types and keeping the old conversion specifier, as in printf("%d", n ? 1 : 2.5), which passes a double for %d and is undefined behaviour; -Wall usually catches it, silence does not mean it is correct.
Omitting the outer parentheses in a MAX-style macro, after which any surrounding operator binds tighter than ?: and quietly steals an arm, producing wrong numbers with no warning.
Try it yourself
Change, predict, then run
Write const char *bucket(int n) that returns "neg" for n < 0, "small" for n < 100 and "big" otherwise as one chained ternary, then add bucket_if() using early returns. Print both results for -5, 0, 42 and 1000 and confirm the two columns match.
Open the C workspaceCheck your understanding
With int i = 2; double d = 0.5; what is the type and value of the expression 1 ? i : d?
- double with value 2.0, because both arms are converted to a common type before one is selected
- int with value 2, because the condition is true so only i is evaluated and only its type matters
- int with value 2, because the type of a conditional expression always comes from its second operand
- double with value 0.5, because the wider arm determines both the type and the value
Show answer
The usual arithmetic conversions apply to the second and third operands, so the expression has type double regardless of which arm runs; 2 converts to 2.0. Option 1 is tempting because it is half true: only one arm is evaluated, but evaluation happens at run time while the type is fixed at compile time from both arms. Believing option 1 is what leads to printf("%d", 1 ? i : d), which passes a double for %d and is undefined behaviour.