Masters-level programming lessons for professional software judgement.

Masters Programmes · Masters Software Engineering · Lesson 13

The Conditional Operator `?:` in C++

Use the conditional operator deliberately when a branch chooses between values, while avoiding unreadable nesting and hidden side effects.

Lesson overview

Programmers often write an if/else solely to assign one of two values. The conditional operator ?: is the expression form of that pattern: it evaluates a condition and produces one of two values. It can make substitution-friendly code concise, but it should be used only when the choice is simple, readable and value-oriented.

LevelMasters
ModeSeminar, expression tracing and readability review
EvidenceA short refactor that converts suitable `if`/`else` assignments into clear conditional expressions and rejects unsuitable cases
SourceConverted from supplied Canvas lesson HTML

Learning objectives

  • Explain the difference between a statement-level if/else and an expression-level conditional operator.
  • Convert a simple two-arm assignment into a safe ?: expression.
  • Use parentheses to make precedence and stream insertion behaviour clear.
  • Describe why only the selected arm of ?: is evaluated.
  • Decide when ?: improves readability and when an ordinary if remains clearer.

Key vocabulary before you start

Conditional operator

The ?: expression that selects between two values.

Expression

Code that produces a value.

Statement

Code that performs an action rather than simply producing a value.

Precedence

The binding strength that decides how an expression groups.

Associativity

The grouping direction for operators with the same precedence.

Side effect

A write, call or output caused while evaluating an expression.

Conditional expression map

?: is useful when the decision is a small value choice. Treat it as a precision tool, not a replacement for every branch.

T1

It produces a value

cond ? a : b evaluates to one of the two arm expressions, so it can appear inside larger expressions.

T2

Keep arms simple

The operator is clearest when both arms are short, side-effect-free values.

T3

Parentheses teach intent

Parenthesise conditional expressions inside streams, function calls and larger expressions to avoid precedence surprises.

T4

Do not hide control flow

If the branch does real work, mutates state or needs explanation, use if/else or named helper functions.

Example: from assignment branch to expression

The original if/else only chooses which value to assign to x, so it can be expressed directly with ?:.

int x = 1;
int y = 2;

if (x > y) {
    x = 2;
} else {
    x = 0;
}

x = (x > y ? 2 : 0);

Trace the state

  1. The condition x > y is evaluated.
  2. If the condition is true, the expression produces 2.
  3. If the condition is false, the expression produces 0.
  4. The produced value is assigned to x.
  5. The operator chooses between values; it does not merely produce true or false.
Line or operationVariableValueState roleInvariant or check
Read the example before running itKey names and callsKnown from sourceReasoning setupEach named element should support the lesson focus: The Conditional Operator ?: in C++.
Trace the first meaningful operationPrimary state or boundaryEstablished by initialisation, call or conditionValid-state checkpointLater reasoning must not use the value before this checkpoint.
Identify the first decision or dereferenceControl or access pointDepends on current stateFailure-mode checkpointThe condition, pointer, argument or dependency must be valid before use.
Record the observable resultReturn value, output or mutationProduced by the exampleEvidence checkpointThe result should match the contract explained in the lesson.

Visual model: expression-selection boundary

Use ?: when the design problem is value selection, not multi-step control flow.

State the conditionName the predicate that selects the value.

Check both armsEnsure both arms are compatible and cheap to understand.

Protect groupingUse parentheses where surrounding operators could confuse meaning.

Reject side effectsUse if when each arm performs actions rather than choosing a value.

Explain readabilityJustify whether the expression is clearer than a branch.

Selection task

For one mini-exercise item, state whether the conditional operator improves or harms readability and why.

From if/else to ?:

An if/else statement controls which statements run. A conditional expression controls which value is produced.

That difference matters. ?: is most appropriate when both branches are simply alternative values for the same expression or assignment.

The expression (cond ? a : b) produces either a or b, not a boolean. Only the chosen arm is evaluated, just as only the chosen branch of an if executes.

Statement form

The branch exists only to assign one of two values.

int x = 1;
int y = 2;

if (x > y) {
    x = 2;
} else {
    x = 0;
}

Expression form

The same value choice can be written directly.

x = (x > y ? 2 : 0);

Applied task

Explain why the conditional operator is appropriate here, and name one situation where it would not be appropriate.

Using ?: inside larger expressions

Because ?: is an expression, it can appear where a value is needed: an initializer, function argument, return statement or stream insertion.

Parentheses are cheap and useful. They make it obvious what the conditional expression covers, especially beside operators such as <<.

Use this power sparingly. If the expression becomes hard to read, introduce a named variable or use an if/else block.

Conditional expression in output and arguments

The value selected by ?: is substituted into the surrounding expression.

#include <iostream>

int main() {
    int x = 3, y = 7;

    std::cout << (x > y ? x : y) << " is greater.\n";

    auto choose = [](int a, int b) { return a + b; };
    int z = choose((x > y ? x : y), 10);

    const char* tag = (x % 2 == 0 ? "even" : "odd");
}

Applied task

Trace the value passed as the first argument to choose when x is 3 and y is 7.

Type rules and lvalue behaviour: practical summary

Both arms should usually produce the same type, or at least obviously compatible types. Mixed arms can trigger conversions that are legal but surprising.

If both arms are lvalues of the same type, the whole conditional expression is also an lvalue. That means you can assign through the selected arm.

This is powerful, but it should be used sparingly in teaching code. Readers must be able to see exactly which object will be modified.

Assigning through the selected lvalue

The assignment targets a when cond is true, otherwise b.

int a = 1, b = 2;
bool cond = true;

(cond ? a : b) = 42; // assigns to 'a' if cond, else to 'b'

Mixed arm types can surprise

0 and 1.5 are compatible, but the result type is double.

auto v = (flag ? 0 : 1.5); // v is double

Applied task

For each conditional expression, state the resulting type and whether the expression is an lvalue.

Precedence and associativity

?: has lower precedence than arithmetic, comparison and stream insertion. Parentheses prevent the reader from having to remember the whole precedence table.

?: is right-associative, so a ? b : c ? d : e parses as a ? b : (c ? d : e).

Nested forms can be technically correct and still too difficult to read. In professional code, clarity beats clever compression.

Parentheses clarify stream output

The stream receives the selected string.

std::cout << (ok ? "yes" : "no") << '\n';

Right-associative nesting

This parses from the right, but a named variable or switch is usually clearer.

auto label = a ? "A" : c ? "C" : "other";
// parses as: a ? "A" : (c ? "C" : "other")

Applied task

Rewrite the nested example with ordinary if statements or named predicates, then compare readability.

Common pitfalls and fixes

Avoid side effects in arms when the expression is meant to be a value choice. A visible if is clearer when each branch performs work.

Avoid mixed types unless the conversion is deliberate and obvious. If the type is important, spell it out with a named variable, explicit cast or helper function.

Avoid long or nested ternaries. A small obvious if is better than a compact expression that slows the reader down.

Unclear mixed types

This compiles, but the resulting type may not be what a novice expects.

auto v = (flag ? 0 : 1.5); // becomes double; maybe surprising

Prefer a named type or named value

Make the conversion deliberate.

double v2 = flag ? 0.0 : 1.5;

Applied task

Choose one pitfall and describe the smallest change that makes the code easier to review.

Only the chosen arm is evaluated

The conditional operator evaluates the condition first, then evaluates only the selected arm.

That makes it safe for simple guarded values, but it also means you should not bury important side effects inside either arm without making that intention obvious.

For teaching and review, ask whether each arm is a value or an action. Values fit ?:; actions usually deserve statements.

Guarded value selection

The division arm is chosen only when the denominator is non-zero.

double safe_divide(double numerator, double denominator) {
    return denominator != 0.0 ? numerator / denominator : 0.0;
}

Applied task

Explain which expression is skipped when denominator is zero, and why that matters.

Worked examples

The best uses of ?: are short, value-focused and easy to substitute into surrounding code.

A maximum-of-two helper is a natural fit because it returns one of two values. A formatter choice can also be a good fit because only one formatting function should run.

Lvalue assignment through ?: is advanced. It is useful to understand, but it should be used only when the selected mutation remains obvious.

Max of two

The expression returns one of the two input values.

int max2(int a, int b) {
    return (a > b ? a : b);
}

Choose formatter lazily

Only the chosen formatting function runs.

std::string format_short();
std::string format_long();
bool brief = /* ... */;

std::string msg = (brief ? format_short() : format_long());

Lvalue assignment through ?:

This mutates left when toLeft is true, otherwise right.

int left = 0, right = 0;
bool toLeft = true;

(toLeft ? left : right) += 10;

Applied task

For each worked example, decide whether ?: is the clearest teaching form or whether an if version would be clearer.

The readability boundary

?: is not a badge of expertise. It is a concise way to express a small value choice.

Avoid nested conditional operators in teaching code unless the goal is specifically to discuss why they are hard to read.

A good review rule is simple: if the reader has to stop and parse the operator structure, use named variables, a helper function or ordinary if/else.

Prefer simple arms

This version is compact without hiding a complicated branch.

const char* label = score >= 50 ? "pass" : "fail";

Use statements when the work is real

Logging, mutation and multi-step behaviour are clearer as statements.

if (score >= 50) {
    record_pass(student);
    notify(student);
} else {
    record_fail(student);
    schedule_support(student);
}

Applied task

Decide which example should remain an if/else and justify the choice in terms of side effects and readability.

Summary checklist

Use ?: when selecting one of two values, especially for simple single assignments, initializers, returns or subexpressions.

Both arms should be simple and same-type where possible. Make mixed types explicit.

Parenthesise when mixing ?: with stream insertion, assignment or larger expressions.

Only the chosen arm runs. Use that deliberately for guarded value selection, not to hide important side effects.

Avoid nested or long ternaries. Prefer if or switch when the logic grows.

?: is an expression, not a statement, so it is ideal for variable substitution when the substitution remains readable.

Applied task

Use the checklist to decide whether one branch from your own code should become ?: or stay as an if.

Applied case lab

Case 1: Assignment-only branch

A code review finds five if/else blocks that only assign one of two literal values. Decide which ones become clearer as ?: and which need named predicates first.

Case 2: Stream insertion surprise

A learner writes std::cout << x > y ? x : y; and gets unexpected behaviour or a compile error. Explain how parentheses make the intended conditional value explicit.

Case 3: Hidden side effects

A developer uses condition ? save() : rollback() inside a larger expression. Decide whether this hides too much control flow and propose a clearer rewrite.

Applied task: choose the value, not the branch

Practise converting simple value-selection branches into ?: while rejecting cases where readability or side effects make if clearer.

Stage 1: read and classify

A. Convert a simple assignment

The branch only assigns one of two values.

int result;
if (score >= 50) {
    result = 1;
} else {
    result = 0;
}
  • Convert the branch to a conditional expression.
  • Explain what value the expression produces when score is 49.
  • Decide whether a named predicate would improve readability.
Reveal one possible refactor
const bool passed = score >= 50;
int result = passed ? 1 : 0;
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

Stage 2: trace or repair

B. Parenthesise inside a stream expression

Make the selected value explicit before it is inserted into the stream.

std::cout << x > y ? x : y;
  • Add parentheses so the stream receives the selected value.
  • Explain why the unparenthesised form is risky.
  • Trace the output when x = 3 and y = 7.
Reveal one possible refactor
std::cout << (x > y ? x : y);
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

Stage 3: justify the design

C. Use ?: as a function argument

Select one argument before calling the function.

auto choose = [](int a, int b) { return a + b; };
int x = 3, y = 7;
int z = choose((x > y ? x : y), 10);
  • State the first argument passed to choose.
  • State the final value of z.
  • Rewrite with a named larger variable if that would be clearer for a novice.
Reveal one possible refactor
const int larger = x > y ? x : y;
int z = choose(larger, 10);
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

Stage 4: test the boundary

D. Use ?: as an initializer

Choose a small tag value at the point of declaration.

int x = 3;
const char* tag = (x % 2 == 0 ? "even" : "odd");
  • State the value of tag when x is 3.
  • Explain why this is a good use of ?:.
  • Rewrite using std::string_view if you prefer a modern library type.
Reveal one possible refactor
std::string_view tag = (x % 2 == 0 ? "even" : "odd");
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

Stage 5: evaluate alternatives

E. Reject an unsuitable ternary

This branch performs actions, not just value selection.

if (ok) {
    write_file();
    log_success();
} else {
    report_error();
}
  • Explain why this should remain statement-level control flow.
  • Identify the side effects in each branch.
  • Suggest a helper function only if it improves naming and testability.
Reveal one possible refactor
Keep the `if`/`else`. The branches perform actions and communicate control flow more clearly as statements.
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

F. Check arm types

Legal conversions can still surprise readers.

auto v = (flag ? 0 : 1.5);
  • State the resulting type of v.
  • Rewrite so the intended type is explicit.
  • Explain why matching arm types helps code review.
Reveal one possible refactor
const double v = flag ? 0.0 : 1.5;
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

G. Assign through an lvalue conditional

Both arms are lvalues of the same type, so the selected object can be assigned.

int a = 1, b = 2;
bool cond = false;
(cond ? a : b) = 42;
  • State which variable changes when cond is false.
  • Explain why the whole expression is an lvalue.
  • Decide whether this is clearer than an if for a novice audience.
Reveal one possible refactor
if (cond) {
    a = 42;
} else {
    b = 42;
}
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

H. Replace a nested ternary

Right-associative nesting is compact but often poor teaching code.

auto label = a ? "A" : c ? "C" : "other";
  • Show how the expression parses.
  • Rewrite using ordinary if statements.
  • Explain which version is easier to debug.
Reveal one possible refactor
const char* label = "other";
if (a) {
    label = "A";
} else if (c) {
    label = "C";
}
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

I. Convert and embed

Convert the branch to ?:, then use it as an initializer.

int score = /* ... */;
std::string label;
if (score >= 50) {
    label = "pass";
} else {
    label = "fail";
}
  • Convert the assignment to a conditional initializer.
  • State the type of label.
  • Explain why this conversion preserves behaviour.
Reveal one possible refactor
std::string label = (score >= 50 ? "pass" : "fail");
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

J. Use ?: as a function argument

Choose the logging level at the call site.

void log_level(const char*);
bool verbose = /* ... */;

log_level(verbose ? "debug" : "info");
  • State which argument is passed when verbose is true.
  • Explain why both arms have compatible types.
  • Add parentheses if that improves readability in your house style.
Reveal one possible refactor
log_level((verbose ? "debug" : "info"));
Model reasoning

A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.

Quick checks

1. What does (condition ? a : b) produce?

2. When is ?: usually clearest?

3. Why should you parenthesise ?: inside a stream expression?

4. Which arm of condition ? a : b is evaluated?

5. What happens if both arms of ?: are lvalues of the same type?

6. How does a ? b : c ? d : e associate?

7. What is the safer fix for auto v = (flag ? 0 : 1.5); if the intended result is floating point?

8. Which example is the best fit for ?:?

9. Why is brief ? format_short() : format_long() lazy?

AI-augmented practice notes

Use these as prompts for disciplined support, not as permission to outsource judgement.

Refactor if/else to ?:
  • Paste a short if/else that sets one variable and ask AI to convert it to a safe conditional expression.
  • Ask AI to explain whether the result is more readable or merely shorter.
  • Have AI check that both arms are simple values rather than hidden side effects.
Parentheses and precedence check
  • Ask AI to add parentheses to conditional expressions inside streams, function calls and arithmetic expressions.
  • Ask it to explain what the compiler groups first.
  • Compare the parenthesised version with a named intermediate variable.
Where to use ?:
  • Ask AI to review a line and suggest parentheses based on operator precedence, especially with << or assignment.
  • Ask AI to propose a named if alternative if readability gets worse with ?:.
  • Keep ?: for initializers, arguments, returns and short substitutions where it remains readable.
Check arm types
  • Paste a conditional expression and ask AI to explain the resulting type.
  • Ask it to identify implicit conversions between the arms.
  • Have it suggest explicit casts, clearer literals or overloads if the type is surprising.
Should this be ?:?
  • Ask AI to apply a quick rubric: short, single assignment or result, both arms simple and same type means probably OK.
  • Ask AI to prefer if when the arms have side effects, the expression is nested or the types are surprising.
  • Use the rubric as a review prompt rather than an automatic rule.
Suggest the clearest form
  • Give AI your example and ask for both a ?: version and an if version.
  • Choose the clearer version for teaching, not merely the shorter version.
  • Ask AI to explain what a novice might misunderstand.
Validate conversions
  • Ask AI to verify that converted snippets preserve behaviour and resulting types.
  • Ask it to flag hidden side effects in either arm.
  • Have it produce two tiny tests, one for each selected arm.
Replace nested ?:
  • Ask AI to rewrite nested ternaries into a switch, ordinary if statements or named predicates.
  • Ask it to preserve behaviour while making the trace easier to debug.
  • Compare the result with the original and keep the version a human can read fastest.
Side-effect audit
  • Ask AI to classify each arm of a conditional expression as a value or an action.
  • If an arm mutates state, ask for an if/else rewrite.
  • Use AI to produce tests for both selected arms.
Readability reviewer
  • Ask AI to score a conditional expression for readability.
  • Ask it to replace nested ?: expressions with named variables or guard clauses.
  • Keep the version that is easiest to trace under code review.

Assessment tasks

  1. Find one if/else assignment and convert it to ?: with a named predicate if useful.
  2. Find one conditional expression that needs parentheses and document the precedence issue.
  3. Rewrite one conditional expression using a named intermediate variable and compare readability.
  4. Identify one branch that should not be converted to ?: because its arms perform actions.
  5. Create two tests that exercise both arms of a conditional expression.
  6. Find one conditional expression with mixed arm types and make the intended result type explicit.
  7. Rewrite one nested ternary as an if/else or switch and compare traceability.
  8. Explain one lvalue conditional assignment, then decide whether it is readable enough for teaching code.
  9. Write a max2 helper with ?:, then write the equivalent if version and compare clarity.
  10. Use ?: as a function argument and verify both arms have compatible types.
  11. Apply the short/simple/same-type rubric to three candidate ternary expressions.

Judgement questions

How is ?: different from if/else?

Focus on expression versus statement, value production and substitution into larger expressions.

When does concision harm clarity?

Discuss nested ternaries, side effects and cases where a named variable or ordinary branch tells the story better.

How can AI help without making this too clever?

Use AI to propose refactors and tests, then judge whether the result is actually easier for a human to review.