Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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 statementconditional operator
Isa statementan expression
Selectswhich code runswhich value is produced
Has a type—yes
Can initialize a constnoyes

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.

Azvya Education Pvt. Ltd.VLSI Mentor
oneside.cpp — complete program
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    const int timeout = fast_mode ? 10 : 1000;

The if version needs four lines and cannot use const:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    ready ? start_transfer() : log_error();        // don't

Nesting. The operator is right-associative, so chains work:

Azvya Education Pvt. Ltd.VLSI Mentor
nested.cpp — the chain parses, and reads poorly past two levels
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
50 -> slow
500 -> medium
3200 -> fast

That 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:

Azvya Education Pvt. Ltd.VLSI Mentor
target.cpp — complete program
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a = 1   b = 99

When 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.

Azvya Education Pvt. Ltd.VLSI Mentor
types.cpp — complete program
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int:int    -> 1
int:double -> 1   is double? true
int:char   -> 1   is int? true

Look 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    auto x = flag ? 1 : std::string("two");
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
mixed.cpp — complete program. Clean at -Wall -Wextra -Wpedantic.
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
value = 4294967295

Walk through what happened:

  1. use_offset is true, so the branch taken is offset, whose value is −1.
  2. The expression's type is the common type of int and unsigned int, which Chapter 4.2 §4 says is unsigned int.
  3. −1 converted to unsigned int is 4294967295 — Chapter 3.3 §4's modulo rule.
  4. v is unsigned int and 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

  1. The conditional operator makes a decision into an expression, so it has a type.
  2. That is why it can initialize a const where an if statement cannot.
  3. Exactly one branch is evaluated — measured: b() called zero times.
  4. It is right-associative, so chains group rightward — Chapter 4.9 §4.
  5. << binds more tightly, so it needs parentheses inside an output statement.
  6. When both branches name objects of one type, the result can be assigned to; measured, b became 99.
  7. The type is the common type of both branches, decided before the condition is known.
  8. Measured: flag ? 1 : 2.5 has type double even when the int branch runs.
  9. Measured: flag ? 1 : 'a' has type int — Chapter 4.2 §3's promotion.
  10. No common type is a compile error, which is the good outcome.
  11. Measured and silent: use_offset ? offset : limit with offset = -1 and limit = 4u produced 4294967295 with zero warnings.
  12. That is Chapter 4.10 §2's cause again — a conversion nobody wrote — in a construct that does not look like arithmetic.
  13. 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.5 is double; flag ? 1 : 'a' is int; 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 if once 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.