Free Handbook · Every example compiled & verified

Flow Control

if/else, the ternary operator, switch with fall-through, while, do-while, for and range-based for loops, break and continue, and if with an initializer.

0 / 145 lessons🔥 0 day streak
ShareXLinkedIn

Module 02 · what you'll be able to do

  • Write if / else if chains and ternaries, and avoid the = versus == bug
  • Use switch correctly: break, deliberate fall-through with [[fallthrough]], and what it cannot switch on
  • Choose between while, do-while, a counted for and a range-based for, and trace a loop by hand
  • Control loops with break and continue, including leaving nested loops cleanly
  • Keep variables scoped tightly with C++17 if and switch initializers
01

if, else if and else

An if runs its block only when the condition is true. Chain alternatives with else if; the first true branch wins and the rest are skipped. The condition can be any expression convertible to bool: numbers are true when non-zero, which is convenient and, as the error card below shows, occasionally dangerous.

C++main.cpp
#include <iostream>

int main() {
    int scores[] = {95, 72, 58, 81};
    for (int score : scores) {
        char grade;
        if (score >= 90) {
            grade = 'A';
        } else if (score >= 75) {
            grade = 'B';
        } else if (score >= 60) {
            grade = 'C';
        } else {
            grade = 'F';
        }
        std::cout << score << " -> " << grade << '\n';
    }
}
Outputcompiled & run with real C++
95 -> A
72 -> C
58 -> F
81 -> B
Your turn

Order matters: move the score >= 60 branch to the top and predict what every score gets before you run it.

Combining conditions

&& (and) and || (or) short-circuit: if the left side already decides the answer, the right side is never evaluated. That makes if (i < v.size() && v[i] > 0) safe, because the index is only used after it has been checked.

C++main.cpp
#include <iostream>
#include <vector>

bool loud(bool value, const char* name) {
    std::cout << "checked " << name << '\n';
    return value;
}

int main() {
    std::vector<int> v{4, -2};
    std::size_t i = 5;
    if (i < v.size() && v[i] > 0) {          // v[5] is never read
        std::cout << "positive\n";
    } else {
        std::cout << "out of range or not positive\n";
    }

    if (loud(true, "left") || loud(false, "right")) {
        std::cout << "or: done after the left side\n";
    }
    bool isAdmin = false, isOwner = true;
    std::cout << std::boolalpha << (!isAdmin && isOwner) << '\n';
}
Outputcompiled & run with real C++
out of range or not positive
checked left
or: done after the left side
true
Error you will hit

using the result of an assignment as a condition

C++
#include <iostream>

int main() {
    int stock = 0;
    if (stock = 5) {
        std::cout << "in stock\n";
    }
}
main.cpp:5:15: warning: using the result of an assignment as a condition without parentheses [-Wparentheses]
    5 |     if (stock = 5) {
      |         ~~~~~~^~~
main.cpp:5:15: note: place parentheses around the assignment to silence this warning
    5 |     if (stock = 5) {
      |               ^
      |         (        )
main.cpp:5:15: note: use '==' to turn this assignment into an equality comparison
    5 |     if (stock = 5) {
      |               ^
      |               ==
Why the compiler said that

A single = assigns 5 to stock, and the value of the assignment (5) is non-zero, so the condition is always true. The program compiles, runs and prints "in stock" even though the stock was 0. It is only a warning, so you see it only with -Wall.

The fix

Use == to compare. Building with -Wall -Werror turns this into a hard error so it can never ship.

C++
#include <iostream>

int main() {
    int stock = 0;
    if (stock == 5) {
        std::cout << "in stock\n";
    }
}
02

The ternary operator

condition ? a : b is an expression: it produces a value, so it can sit inside an initializer, a function argument or a << chain where an if statement cannot. Both branches must have compatible types. Use it for a simple choice between two values; if either branch needs a statement, write an if.

C++main.cpp
#include <iostream>
#include <string>

int main() {
    int items = 1;
    std::cout << items << (items == 1 ? " item" : " items") << '\n';

    int temperature = 31;
    const std::string advice = temperature > 30 ? "stay hydrated" : "enjoy the weather";
    std::cout << advice << '\n';

    int a = 14, b = 9;
    int larger = a > b ? a : b;
    std::cout << "larger: " << larger << '\n';
}
Outputcompiled & run with real C++
1 item
stay hydrated
larger: 14
Your turn

The ternary is what lets advice be const: with an if you would have to declare it first and assign later. Rewrite it with an if to feel the difference.

Parenthesise a ternary inside <<
<< binds tighter than ?:, so std::cout << x > 0 ? "pos" : "neg"; does not mean what it looks like and does not compile. Wrap the whole ternary in parentheses, as the example does. Nested ternaries (a ? b : c ? d : e) are legal but hard to read; reviewers usually ask for an if chain instead.
03

switch, break and [[fallthrough]]

switch jumps straight to the case label that matches an integer, char or enum value. Execution then continues downwards through the following cases until it hits a break. That fall-through is occasionally useful and frequently a bug, so C++17 added the [[fallthrough]]; attribute to mark the deliberate ones. default handles every value no case matched.

C++main.cpp
#include <iostream>

int main() {
    char commands[] = {'s', 'p', 'x', 'q'};
    for (char c : commands) {
        switch (c) {
            case 's':
                std::cout << "start\n";
                break;
            case 'p':
                std::cout << "pause, ";
                [[fallthrough]];         // deliberate: pause also saves
            case 'v':
                std::cout << "save\n";
                break;
            case 'q':
            case 'Q':                    // two labels, one action
                std::cout << "quit\n";
                break;
            default:
                std::cout << "unknown '" << c << "'\n";
        }
    }
}
Outputcompiled & run with real C++
start
pause, save
unknown 'x'
quit
Your turn

Delete the break after "start" and predict the output for 's'. Then build with -Wimplicit-fallthrough and read the warning clang gives you.

What switch cannot do

  • No strings. The value must be an integer, char, bool or enum. For strings use an if chain or a std::unordered_map from string to action.
  • No ranges. case 1 ... 5: is a GCC/Clang extension, not standard C++. Portable code uses an if chain for ranges.
  • Case values must be compile-time constants. case limit: only works if limit is constexpr.
  • No jumping over an initialization. Declaring a variable inside a case needs its own { } block.
Error you will hit

statement requires expression of integer type

C++
#include <iostream>
#include <string>

int main() {
    std::string cmd = "stop";
    switch (cmd) {
        case "start": std::cout << "go\n"; break;
        case "stop":  std::cout << "halt\n"; break;
    }
}
main.cpp:6:5: error: statement requires expression of integer type ('std::string' (aka 'basic_string<char>') invalid)
    6 |     switch (cmd) {
      |     ^       ~~~
main.cpp:7:14: error: value of type 'const char[6]' is not implicitly convertible to 'int'
    7 |         case "start": std::cout << "go\n"; break;
      |              ^~~~~~~
main.cpp:8:14: error: value of type 'const char[5]' is not implicitly convertible to 'int'
    8 |         case "stop":  std::cout << "halt\n"; break;
      |              ^~~~~~
Why the compiler said that

switch compiles to a jump table or a series of integer comparisons, so it only accepts integral and enum types. A std::string is a class, and a string literal is a character array.

The fix

Compare strings with an if / else if chain (or map each string to an enum first and switch on that).

C++
#include <iostream>
#include <string>

int main() {
    std::string cmd = "stop";
    if (cmd == "start") {
        std::cout << "go\n";
    } else if (cmd == "stop") {
        std::cout << "halt\n";
    }
}
Error you will hit

cannot jump from switch statement to this case label

C++
#include <iostream>

int main() {
    int choice = 2;
    switch (choice) {
        case 1:
            int bonus = 10;
            std::cout << bonus << '\n';
            break;
        case 2:
            std::cout << "two\n";
            break;
    }
}
main.cpp:10:9: error: cannot jump from switch statement to this case label
   10 |         case 2:
      |         ^
main.cpp:7:17: note: jump bypasses variable initialization
    7 |             int bonus = 10;
      |                 ^
Why the compiler said that

All the cases share one scope. bonus is visible from its declaration to the closing brace of the switch, including inside case 2. Jumping to case 2 would skip its initialization and leave a visible variable with garbage in it, so the compiler forbids it.

The fix

Give the case its own block so the variable's scope ends before the next label.

C++
#include <iostream>

int main() {
    int choice = 2;
    switch (choice) {
        case 1: {
            int bonus = 10;
            std::cout << bonus << '\n';
            break;
        }
        case 2:
            std::cout << "two\n";
            break;
    }
}
Error you will hit

case ranges are a GNU extension

C++
#include <iostream>

int main() {
    int score = 7;
    switch (score) {
        case 0 ... 4: std::cout << "low\n"; break;
        case 5 ... 10: std::cout << "high\n"; break;
    }
}
main.cpp:6:16: error: case ranges are a GNU extension [-Werror,-Wgnu-case-range]
    6 |         case 0 ... 4: std::cout << "low\n"; break;
      |                ^
main.cpp:7:16: error: case ranges are a GNU extension [-Werror,-Wgnu-case-range]
    7 |         case 5 ... 10: std::cout << "high\n"; break;
      |                ^
Why the compiler said that

With default flags clang and g++ quietly accept this and print high, which is how it sneaks into code bases. It is not standard C++, so MSVC rejects it outright. The message above is what you get with -pedantic-errors, the flag that makes the compiler enforce the standard.

The fix

Use an if chain for ranges; it is portable and just as fast.

C++
#include <iostream>

int main() {
    int score = 7;
    if (score >= 0 && score <= 4) {
        std::cout << "low\n";
    } else if (score >= 5 && score <= 10) {
        std::cout << "high\n";
    }
}
04

while and do-while

A while loop checks its condition before each pass, so it may run zero times. Use it when you do not know the number of iterations up front: "keep halving until it is small", "keep reading until the input ends". A do-while checks after each pass, so its body always runs at least once, which fits "show a menu, then ask whether to repeat".

C++main.cpp
#include <iostream>

int main() {
    // while: how many times can 1000 be halved before it drops below 10?
    int value = 1000;
    int halvings = 0;
    while (value >= 10) {
        value /= 2;
        ++halvings;
    }
    std::cout << halvings << " halvings, ending at " << value << '\n';

    // do-while: the body runs once even though the condition is false
    int attempts = 5;
    do {
        std::cout << "attempt " << attempts << '\n';
        ++attempts;
    } while (attempts < 3);
}
Outputcompiled & run with real C++
7 halvings, ending at 7
attempt 5
C++main.cpp
#include <iostream>

int main() {
    int n = 0, sum = 0, count = 0;
    while (std::cin >> n) {        // false when input ends or is not a number
        sum += n;
        ++count;
    }
    std::cout << count << " numbers, sum " << sum << '\n';
}
Outputcompiled & run with real C++
4 numbers, sum 54

Fed the input 12 7 30 5. std::cin >> n returns the stream, and a stream converts to false once a read fails. This is the idiomatic "read until the end" loop, the one competitive programmers and command-line tools use.

Infinite loops
If nothing inside a while changes the condition, it never ends. Before you run a loop, point at the line that moves it towards stopping. In a terminal, Ctrl+C kills a runaway program.
05

Counted for loops

A for loop puts the three parts of a counted loop on one line: for (init; condition; step). The init runs once, the condition is checked before every pass, and the step runs after every pass. A variable declared in the init exists only inside the loop.

C++main.cpp
#include <iostream>

int main() {
    for (int i = 1; i <= 5; ++i) {
        std::cout << i << " squared is " << i * i << '\n';
    }

    // Counting down, in steps of 3
    for (int t = 9; t > 0; t -= 3) {
        std::cout << t << ' ';
    }
    std::cout << '\n';

    // Two variables in one loop: walk inward from both ends
    for (int lo = 0, hi = 6; lo < hi; ++lo, --hi) {
        std::cout << '(' << lo << ',' << hi << ") ";
    }
    std::cout << '\n';
}
Outputcompiled & run with real C++
1 squared is 1
2 squared is 4
3 squared is 9
4 squared is 16
5 squared is 25
9 6 3
(0,6) (1,5) (2,4)
Your turn

Print a multiplication table for 1 to 4 using one for loop inside another. Each row should be one line.

VisualizeSumming the even numbers below 7Step 1 / 9
int sum = 0;
for (int i = 0; i < 7; ++i) {
if (i % 2 != 0) continue;
sum += i;
}
std::cout << sum;
Line 1

sum starts at 0.

Variables now
sum0
All 9 steps as a table
StepLineWhat happenedVariables now
11sum starts at 0.sum = 0
22Init runs once: i = 0. Condition 0 < 7 is true.i = 0
340 is even, so it is added.sum = 0
43Step makes i 1. It is odd, so continue jumps straight to the step.i = 1
54i is 2: added.i = 2 sum = 2
64i 3 is skipped; i 4 is added.i = 4 sum = 6
74i 5 is skipped; i 6 is added.i = 6 sum = 12
82Step makes i 7. Condition 7 < 7 is false, so the loop ends and i goes out of scope.i = 7
96Prints the total.
Error you will hit

for loop has empty body

C++
#include <iostream>

int main() {
    for (int i = 0; i < 3; i++);
    {
        std::cout << "tick\n";
    }
}
main.cpp:4:32: warning: for loop has empty body [-Wempty-body]
    4 |     for (int i = 0; i < 3; i++);
      |                                ^
main.cpp:4:32: note: put the semicolon on a separate line to silence this warning
Why the compiler said that

The stray ; is an empty statement, and it is the whole body of the loop. The loop spins three times doing nothing, then the block below runs once as ordinary code and prints a single "tick".

The fix

Remove the semicolon so the block becomes the loop body.

C++
#include <iostream>

int main() {
    for (int i = 0; i < 3; i++) {
        std::cout << "tick\n";
    }
}
06

Range-based for loops

When you want every element of a container and do not need the index, for (auto x : container) is shorter and removes the off-by-one and signed/unsigned bugs of a counted loop. How you declare the loop variable matters: a plain auto gets a copy of each element, auto& gets a reference you can modify, and const auto& reads without copying.

C++main.cpp
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<int> prices{100, 250, 80};

    for (int p : prices) {                // p is a COPY of each element
        p *= 2;
        std::cout << p << ' ';
    }
    std::cout << '\n';
    for (int p : prices) std::cout << p << ' ';   // unchanged
    std::cout << '\n';

    for (int& p : prices) p *= 2;         // reference: changes the vector
    for (int p : prices) std::cout << p << ' ';
    std::cout << '\n';

    std::vector<std::string> names{"Asha", "Ravi", "Meera"};
    for (const auto& name : names) {      // no copy of each string
        std::cout << name << " (" << name.size() << ")\n";
    }

    for (char c : std::string{"C++"}) std::cout << '[' << c << ']';
    std::cout << '\n';
}
Outputcompiled & run with real C++
200 500 160
100 250 80
200 500 160
Asha (4)
Ravi (4)
Meera (5)
[C][+][+]
Your turn

Add for (auto x : {3, 1, 4}) — a range-based loop works over a braced list too.

Which declaration to use
const auto& for reading anything bigger than a number (strings, structs), auto& when you are changing the elements, plain auto for small values like int and char. Do not add or remove elements from a vector while looping over it with a range-based for: the loop keeps iterators that growth invalidates. Module 04 shows the safe patterns.
07

break, continue and nested loops

break leaves the innermost loop (or switch) immediately. continue skips the rest of the current pass and goes to the next one (in a for loop, the step still runs). Neither can reach past the innermost loop, so to leave two nested loops at once you either use a flag, or better, move the loops into a function and return.

C++main.cpp
#include <iostream>
#include <vector>

// Returning from a function is the cleanest way out of nested loops
bool findPair(const std::vector<int>& v, int target) {
    for (std::size_t i = 0; i < v.size(); ++i) {
        for (std::size_t j = i + 1; j < v.size(); ++j) {
            if (v[i] + v[j] == target) {
                std::cout << v[i] << " + " << v[j] << " = " << target << '\n';
                return true;
            }
        }
    }
    return false;
}

int main() {
    std::vector<int> orders{120, -1, 45, 0, 300, 999, 60};
    int total = 0;
    for (int amount : orders) {
        if (amount == 999) break;        // sentinel: stop reading
        if (amount <= 0) continue;       // skip invalid entries
        total += amount;
    }
    std::cout << "total " << total << '\n';

    if (!findPair({4, 9, 2, 7}, 11)) std::cout << "no pair\n";
}
Outputcompiled & run with real C++
total 465
4 + 7 = 11
Your turn

Change the target to 100 so no pair matches. Then write the same search without a function, using a bool found flag checked in the outer loop, and compare how readable the two are.

08

if and switch with an initializer

Since C++17 you can declare a variable inside the if itself: if (init; condition). The variable exists in the condition and in both the if and else blocks, and disappears after. This keeps temporary values like a search result from leaking into the rest of the function. switch (init; value) works the same way.

C++main.cpp
#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> stock{{"pen", 40}, {"ink", 0}};

    for (const std::string item : {"pen", "ink", "paper"}) {
        if (auto it = stock.find(item); it == stock.end()) {
            std::cout << item << ": not sold here\n";
        } else if (int qty = it->second; qty > 0) {
            std::cout << item << ": " << qty << " left\n";
        } else {
            std::cout << item << ": sold out\n";
        }
        // 'it' and 'qty' do not exist here
    }

    switch (int code = 404; code / 100) {
        case 2: std::cout << "success\n"; break;
        case 4: std::cout << "client error " << code << '\n'; break;
        default: std::cout << "other\n";
    }
}
Outputcompiled & run with real C++
pen: 40 left
ink: sold out
paper: not sold here
client error 404

std::map::find returns end() when the key is missing, which is why the first condition compares against it. Maps are covered properly in Module 13; the point here is that it and qty cannot be misused after the if.

Your turn

Try printing it->first after the if/else chain. The compiler says it is undeclared, which is exactly the protection you wanted.

Short-circuit evaluation
&& stops at the first false, || at the first true; the right side is not evaluated.
Ternary operator
cond ? a : b, an expression that yields one of two values.
Fall-through
In a switch, running on into the next case because there was no break.
[[fallthrough]]
C++17 attribute that marks a deliberate fall-through and silences -Wimplicit-fallthrough.
Range-based for
for (decl : range), visits every element; use auto& to modify, const auto& to read without copying.
Sentinel
A special value (such as 999 or end of input) that tells a loop to stop.
if with initializer
if (init; cond) (C++17), declares a variable scoped to the if/else only.
Quick check

In a switch, case 1: prints "one" and has no break; case 2: prints "two" and breaks. The value is 1. What is printed?

Quick check

You want to double every element of std::vector<int> v in place. Which loop works?

Frequently asked questions

Can you use a string in a C++ switch statement?
No. A C++ switch only works on integers, characters, bool and enums. For strings use an if / else if chain, or map the strings to an enum or to functions in a std::unordered_map.
What is the difference between while and do-while in C++?
A while loop checks its condition before each pass and may run zero times. A do-while checks after each pass, so its body always runs at least once.
How do I break out of two nested loops in C++?
break only leaves the innermost loop. Put the nested loops in a function and return when you are done, or set a bool flag that the outer loop checks. Avoid goto except in rare generated code.

Finish the C++ handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.