C++ Part I · Module 5 · Control Flow
The Conditional Operator
Chapter 5.1 built a decision out of statements. This chapter builds one out of an expression instead, which sounds like a formatting preference and is not. An if statement selects which code runs; a conditional operator produces a value, and a value has to have a type. That single difference is where the chapter's weight sits, because C++ does not give the expression the type of whichever branch happened to run — it works out one type that both branches can agree on, and uses that regardless. Most of the time the answer is what you expected and nothing happens. Occasionally it is not, and because the rule is the same one Chapter 4.2 applies to arithmetic, the failure looks exactly like Chapter 4.4's: a branch returned negative one, the expression produced four billion, and the compiler said nothing at all. The rest of the chapter is about judgement — the conditional operator is genuinely clearer than an if statement in a narrow set of cases, and genuinely worse in the cases people reach for it most.
Foundation15 min readC++Part IFundamentalsControl Flow
Part I · Module 5 · Chapter 5.2 · The Conditional Operator
1. A Decision That Produces a Value
The conditional operator is written with two symbols and takes three operands:
const char* state = ready ? "running" : "stalled";Read it as a question: is ready true? If so "running", otherwise "stalled".
It is the only operator in C++ that takes three operands, which is why you will hear it called the ternary operator. That name describes its shape rather than its job; this curriculum calls it the conditional operator, which describes what it does.
The difference from Chapter 5.1 is not cosmetic:
if statement | conditional operator | |
|---|---|---|
| Is | a statement | an expression |
| Selects | which code runs | which value is produced |
| Has a type | — | yes |
Can initialize a const | no | yes |
That third row is the whole chapter. An expression has a type, a type has to be decided, and the way C++ decides it is not the obvious way.
2. Exactly One Branch Is Evaluated
Before the type rules, one guarantee. The condition is evaluated first, and then exactly one of the two branches — never both.
#include <iostream>
static int calls_a = 0, calls_b = 0;
static int a() { ++calls_a; return 1; }
static int b() { ++calls_b; return 2; }
int main()
{
bool flag = true;
int v = flag ? a() : b();
std::cout << "value " << v << " a() called " << calls_a
<< " time(s), b() called " << calls_b << " time(s)\n";
return 0;
}Expected output:
value 1 a() called 1 time(s), b() called 0 time(s)b() never ran. This is the same kind of guarantee Chapter 4.5 §3 established for && and ||, and Chapter 4.9 §6 listed the conditional operator alongside them for exactly this reason. It means the untaken branch is safe to write even when evaluating it would be wrong — a point §5 relies on.
3. Where It Genuinely Reads Better
The honest case for the conditional operator is narrow and real: one value, chosen two ways, used immediately.
const int timeout = fast_mode ? 10 : 1000;The if version needs four lines and cannot use const:
int timeout; // not const: it has to be assigned later
if (fast_mode) timeout = 10;
else timeout = 1000;That is worse in a way Chapter 3.9 named. The second version creates an object, leaves it uninitialized for a moment, and then writes to it — so it cannot be const, and Chapter 3.8's whole argument applies to the window in between. The conditional operator initializes it once, correctly, and lets it be immutable for the rest of its life.
The same argument makes it the natural form inside an output statement or a call, where a statement cannot go at all:
std::cout << "mode: " << (fast_mode ? "fast" : "normal") << '\n';Note the parentheses. Chapter 4.9 §3 listed the precedence trap: << binds more tightly than ?:, so without them this does not parse the way you want. This is one of the cases where the compiler does help — dropping them gives:
warning: operator '?:' has lower precedence than '<<'; '<<' will be evaluated first [-Wparentheses]
error: invalid operands to binary expression ('const char[7]' and 'char')The warning names the rule, and in this particular line it is also a hard error, because the mis-grouping leaves "normal" << '\n' — a pointer shifted by a character — which has no meaning. A line that mis-groups into something valid would only get the warning.
Use it when the result is one value you are about to name or print. Stop using it when the branches start doing work.
4. Where It Reads Worse
Three shapes to avoid, in increasing order of how often they appear in real code.
Branches with side effects. If both branches do something rather than produce something, the construct is being used as a statement and should be one:
ready ? start_transfer() : log_error(); // don'tNesting. The operator is right-associative, so chains work:
#include <iostream>
static const char* grade(int mhz)
{
return mhz < 100 ? "slow" : mhz < 1000 ? "medium" : "fast";
}
int main()
{
for (int m : {50, 500, 3200}) std::cout << m << " -> " << grade(m) << '\n';
return 0;
}Expected output:
50 -> slow
500 -> medium
3200 -> fastThat is correct and it is the same chain Chapter 5.1 §4 wrote with else if. The right-associativity means it groups as mhz < 100 ? "slow" : (mhz < 1000 ? "medium" : "fast") — Chapter 4.9 §4's rule. Two levels is readable. Three is a chain pretending not to be one, and Chapter 5.1's else if form says it plainly.
Long branches. Once either branch does not fit comfortably on the line, the reader has to find the : before they can parse anything, and an if is clearer.
5. It Can Be Assigned To
One behaviour worth knowing because it surprises people, and because the untaken branch is genuinely untouched:
#include <iostream>
int main()
{
int a = 1, b = 2;
bool pick_a = false;
(pick_a ? a : b) = 99; // the chosen operand is the assignment target
std::cout << "a = " << a << " b = " << b << '\n';
return 0;
}Expected output:
a = 1 b = 99When both branches name objects of the same type, the conditional selects the object, not a copy of its value, and that object can be assigned to. The formal account of why belongs to Chapter 13.6, which is where the value-category model arrives; Chapter 4.1 §6 deliberately deferred it and this chapter keeps that deferral.
It is a legal construct that is almost always clearer as an if. Know it so you can read it.
6. The Result Has One Type, and Both Branches Decide It
Now the half of the chapter that matters.
The conditional operator is an expression, so it has a type. That type is not the type of the branch that happened to run — it is a single type that C++ works out from both branches, before knowing which one will be taken.
#include <iostream>
#include <type_traits>
int main()
{
bool flag = true;
// both branches int -> int
std::cout << "int:int -> " << (flag ? 1 : 2) << '\n';
// int and double -> double, even though the int branch is taken
auto mixed = flag ? 1 : 2.5;
std::cout << "int:double -> " << mixed
<< " is double? " << std::boolalpha
<< std::is_same<decltype(mixed), double>::value << '\n';
// int and char -> int
auto ic = flag ? 1 : 'a';
std::cout << "int:char -> " << ic
<< " is int? " << std::is_same<decltype(ic), int>::value << '\n';
return 0;
}Expected output:
int:int -> 1
int:double -> 1 is double? true
int:char -> 1 is int? trueLook at the middle line carefully. flag is true, so the branch that ran was 1, an int. The expression's type is double — measured, not asserted. The value printed is 1 because 1 converted to double is 1.0 and the stream printed it without a fractional part, but the type was double the whole time.
This is not a new rule. It is Chapter 4.2 §4's usual arithmetic conversions, the same mechanism that makes 1 + 2.5 a double. The conditional operator asks the same question of its two branches that + asks of its two operands.
The third line shows the same thing with a promotion: int and char give int, which is Chapter 4.2 §3.
7. When There Is No Common Type
If the two branches have no type they can both convert to, the program does not compile:
auto x = flag ? 1 : std::string("two");error: incompatible operand types ('int' and 'std::string' (aka 'basic_string<char>'))That is the good case. A type error you cannot ignore is better than a conversion you did not notice — which is the next section.
8. The Trap: a Branch Returning −1 Produced 4294967295
Here is the whole chapter in one program, and it compiles without a single warning:
#include <iostream>
int main()
{
bool use_offset = true;
unsigned int limit = 4;
int offset = -1;
auto v = use_offset ? offset : limit; // common type is unsigned
std::cout << "value = " << v << '\n';
return 0;
}Expected output:
value = 4294967295Walk through what happened:
use_offsetistrue, so the branch taken isoffset, whose value is −1.- The expression's type is the common type of
intandunsigned int, which Chapter 4.2 §4 says isunsigned int. - −1 converted to
unsigned intis 4294967295 — Chapter 3.3 §4's modulo rule. visunsigned intand holds 4294967295.
The branch that ran returned −1 and the expression produced four billion. Nothing was wrong with the condition, nothing was wrong with either branch, and no cast appeared anywhere.
And the compiler said nothing. Zero diagnostics at -Wall -Wextra -Wpedantic. Chapter 4.4 §3's signed/unsigned comparison at least gets -Wsign-compare; this gets silence, because a conversion between a signed and an unsigned type is not in itself an error — the compiler has no way to know you did not intend it.
9. What to Actually Do
Use it for one value chosen two ways, especially when that lets the result be const.
Parenthesise it inside any larger expression — << binds tighter, per Chapter 4.9 §3.
Keep both branches short and free of side effects.
Stop at two levels of nesting. Past that, Chapter 5.1's else if is the honest form.
Make both branches the same type. If they are not, either fix the types or write the static_cast yourself.
Never mix signed and unsigned across the branches. §8 is what that costs.
10. Mistakes Worth Making Once
Assuming the result has the type of the branch that ran. Measured: the int branch ran and the type was double.
Mixing signed and unsigned across the branches. Measured: −1 in, 4294967295 out, zero warnings.
Expecting a warning for that. There is none at -Wall -Wextra -Wpedantic.
Omitting the parentheses inside <<. << binds more tightly than ?:.
Using it for side effects. cond ? do_a() : do_b(); is a statement wearing an expression's clothes.
Nesting three or more deep. It parses right-associatively and reads worse than the chain it replaced.
Expecting both branches to be evaluated. Measured: b() was called zero times.
11. Interview Relevance
The conditional operator is the module's lowest-weighted chapter and it still produces one reliable question: what is the type of cond ? x : y?
The answer that gets credit is "the common type of both operands, worked out by the same conversions as arithmetic — not the type of the branch taken". The answer that gets follow-up questions is "the type of whichever branch runs".
If you can add the signed/unsigned example from §8 — a branch returning −1 producing 4294967295 with no warning — you have demonstrated the thing the question is actually probing: that you know implicit conversions happen in places that do not look like arithmetic.
12. What You Should Be Able to Say Now
- The conditional operator makes a decision into an expression, so it has a type.
- That is why it can initialize a
constwhere anifstatement cannot. - Exactly one branch is evaluated — measured:
b()called zero times. - It is right-associative, so chains group rightward — Chapter 4.9 §4.
<<binds more tightly, so it needs parentheses inside an output statement.- When both branches name objects of one type, the result can be assigned to; measured,
bbecame 99. - The type is the common type of both branches, decided before the condition is known.
- Measured:
flag ? 1 : 2.5has typedoubleeven when theintbranch runs. - Measured:
flag ? 1 : 'a'has typeint— Chapter 4.2 §3's promotion. - No common type is a compile error, which is the good outcome.
- Measured and silent:
use_offset ? offset : limitwithoffset = -1andlimit = 4uproduced 4294967295 with zero warnings. - That is Chapter 4.10 §2's cause again — a conversion nobody wrote — in a construct that does not look like arithmetic.
- The guard needs no new knowledge: do not mix signed and unsigned across the branches.
13. Exercises
Exercise 1 — name the type. Give the type of each, where i is int, d is double, c is char, u is unsigned int: (a) f ? i : i (b) f ? i : d (c) f ? c : i (d) f ? i : u.
Exercise 2 — predict the value. With u = 4 and i = -1, give the printed value of f ? i : u for f true and for f false, and explain why both answers are the same kind of surprise.
Exercise 3 — fix it three ways. Repair §8's program so it prints −1, using (a) a type change, (b) a different type change, (c) a static_cast. Say which you would ship and why.
Exercise 4 — which construct? For each, choose the conditional operator or an if, and give one reason: (a) initialize a const int two ways (b) call one of two functions (c) print one of two words mid-sentence (d) pick between two values with a four-line calculation in each branch.
Exercise 5 — count the calls. In flag ? a() : b() with flag false, how many times does each run? Name the chapter that made this a guarantee rather than an observation.
Exercise 6 — add the parentheses. std::cout << "mode: " << fast_mode ? "fast" : "normal" << '\n'; does not do what it looks like. Write out how it groups, say why that grouping is a hard error rather than just a wrong answer, and fix it.
Exercise 7 — the honest version. Rewrite as an else if chain and say at which level of nesting you would have switched: return a ? "w" : b ? "x" : c ? "y" : "z";
14. Summary
- The conditional operator turns a decision into an expression with a type, which is why it can initialize a
const. - Exactly one branch is evaluated — the same guarantee as
&&and||. - It is right-associative, binds more loosely than
<<, and can be assigned to. - Its type is the common type of both branches, not of the branch that ran.
- Measured:
flag ? 1 : 2.5isdouble;flag ? 1 : 'a'isint; no common type is a compile error. - Measured and silent: a branch returning −1 produced 4294967295 because the other branch was
unsigned. - That is a conversion nobody wrote, Chapter 4.10 §2's cause, in a construct that looks like selection.
- Use it for one short value chosen two ways; use
ifonce the branches do work. - Never mix signed and unsigned across the branches.
Both chapters so far have chosen between paths by evaluating conditions one after another until one is true. The next chapter does something different: it takes a single value and jumps straight to the matching label, which lets the compiler check the list of cases in a way it cannot check a chain. It also has the most notorious default behaviour in the language — when a case finishes, execution does not stop, it carries on into the next one — and the measurement that matters is that your compiler does not warn about it at the warning level this curriculum has used since Chapter 1.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
