Chapter 06 of 24 | C++

Decisions and loops

Control flow determines which statements run and how often. Use if and else to choose between paths, and use braces even for a one-statement body so later edits do not change the meaning accidentally.

if (score >= 90) {
    std::cout << "Excellent\n";
} else if (score >= 60) {
    std::cout << "Passing\n";
} else {
    std::cout << "Keep practicing\n";
}

Conditions are Boolean expressions. The order of an if / else if chain matters: the first true branch runs, and the rest are skipped. Use switch when comparing one integral or enumeration value against several fixed cases; include a default when unexpected values need handling.

Repeat work with loops

for (int i = 0; i < 5; ++i) {
    std::cout << i << '\n';
}

int remaining = 3;
while (remaining > 0) {
    std::cout << remaining << '\n';
    --remaining;
}

A for loop places initialization, continuation condition, and update together. A while loop checks before each iteration and is useful when the number of repetitions is not known in advance. A do-while checks after the body, so it always runs at least once.

Make sure each loop can eventually stop. An off-by-one error often comes from confusing the last valid index with the number of elements. When iterating through a container, a range-based for avoids manual index arithmetic:

for (const auto& item : items) {
    std::cout << item << '\n';
}

break exits the nearest loop or switch; continue moves to the next loop iteration. Use them sparingly when a clear condition can express the loop directly.

Build a condition from a question

Before writing an if, describe the decision in ordinary language and identify the exact boundary. “A person may enter if they are at least 18” becomes age >= 18, not age > 18. Test values just below, exactly on, and just above the boundary. This technique catches many off-by-one errors before they spread.

Conditions should answer one clear question. If a conditional combines many unrelated facts, extract named Boolean expressions such as has_valid_ticket and is_venue_open. This makes both the code and a failing test easier to discuss.

Choose the loop that matches the job

Use a counted for loop when a counter has a clear start, stop condition, and update. A while loop fits a process that continues until a changing condition is met, such as reading records. A do-while is useful for a menu that must be shown at least once. The choice should help a reader understand why the loop repeats.

For manual indexing, reason about the loop invariant: at the start of each iteration, what do you know about the index and the values already processed? A common vector loop uses i < values.size(), because the valid indexes stop just before the size. Be careful comparing an int index with the unsigned size type returned by a container; a range-based loop avoids this issue when you do not need the index.

Trace a loop rather than guessing

To debug a loop, make a small table with one row per iteration. Record the condition result, important variable values, and whether the update executes. If the loop never ends, check whether a variable in the condition can change and whether every path reaches that change. If it stops too early, inspect the condition and whether an update or break runs unexpectedly.

Nested loops multiply work. If an outer loop runs n times and an inner loop runs m times for each outer iteration, the body runs approximately n * m times. This is often fine for small data but matters when both sizes grow.

Switch statements and fallthrough

A switch is most useful when one expression is compared against a set of fixed integral or enumeration values. Each case normally ends with break to prevent execution from continuing into the next case. Intentional fallthrough should be explicit and explained. For nontrivial ranges, strings, or pattern-like conditions, an if / else if chain is usually clearer.

Practice

  1. Print the integers from 1 through 20, one per line.
  2. Read numbers until the user enters zero, then report their sum.
  3. Use a loop to count how many values in a collection are negative.
  4. Trace a loop by writing down its condition and counter value before each iteration.
  5. Write a menu that always appears once and repeats until the user selects Quit.
  6. Test a range check at its minimum, one below its minimum, its maximum, and one above its maximum.

Key idea: structure decisions around clear Boolean conditions and ensure every loop has understandable progress and termination.

Loop invariants provide a correctness argument

A loop invariant is a statement that is true before the first iteration and remains true after each iteration. For a sum loop over the first i elements of a vector, an invariant might say that the accumulator equals the sum of precisely those processed elements. At termination, the loop condition tells you how many elements have been processed, and the invariant then establishes the final result.

To reason rigorously, identify initialization, preservation, and termination. Initialization establishes the invariant. The body and update preserve it. A variant—often a nonnegative measure such as remaining elements—decreases toward termination. If no such measure exists, the loop may fail to terminate. These ideas scale from a simple counter to protocol state machines and complex algorithms.

Control flow and proof obligations

For a branch, consider whether the conditions are mutually exclusive and whether their cases cover all possible states. A final else can make a default behavior explicit. With an enum, switch over the domain values and consider how newly added enumerators should be handled; a compiler warning for an unhandled enumerator can help detect an incomplete update.

Early returns can reduce nesting, but a function still needs a clear contract on every path. For each branch, determine whether outputs are initialized and invariants remain true. In larger code, state-machine representations can be easier to verify than several independent Booleans that permit contradictory combinations.

Complexity and loop transformations

Count iterations to estimate complexity, then inspect whether each iteration's work is constant or depends on input size. A loop inside a loop is not always quadratic: if the inner index advances across the entire input only once over all outer iterations, the total may still be linear. Conversely, a nested search can become quadratic even when each individual search looks small.

Compiler optimizations can transform loops, but only within the language's sequencing and observable-behavior rules. Do not reorder operations that communicate through I/O, volatile access, or synchronization without understanding the semantics. First prove the simple loop correct, then measure before attempting manual unrolling or other low-level transformations.

Advanced exercise

  1. Write a loop invariant for a linear search and use it to explain the result when the loop stops.
  2. For a nested-loop algorithm, count total iterations for several input sizes and derive a bound.
  3. Model a small protocol or menu as an explicit state machine and test every transition, including invalid transitions.
0% read