Skip to content

Control Flow

Control flow statements determine the execution path of your code. In Modern C++, control flow constructs have been enhanced to be more expressive, scope-safe, and highly optimization-friendly.


1. Conditional Statements

The if / else Statement

The basic if statement evaluates a boolean expression and executes a block if true.

1
2
3
4
5
6
7
8
int battery_level = 15;
if (battery_level < 20) {
    std::cout << "Warning: Battery low!\n";
} else if (battery_level == 100) {
    std::cout << "Battery fully charged.\n";
} else {
    std::cout << "Battery status normal.\n";
}

C++17 Init-Statements for if

C++17 introduced the ability to declare and initialize variables directly within the scope of the if statement. This keeps the variable local to the branch, preventing it from polluting the surrounding scope.

// Traditional style: 'status' leaks to the parent scope
int status = get_sensor_status();
if (status == 0) {
    std::cout << "Sensor healthy\n";
}
// 'status' is still visible and mutable here, which might cause bugs.

// C++17 Style: 'status' is confined to the if/else block
if (int status = get_sensor_status(); status == 0) {
    std::cout << "Sensor healthy\n";
} else {
    std::cout << "Sensor error code: " << status << "\n";
}
// Compilation error: 'status' is out of scope here

2. The switch Statement

The switch statement selects one of many code blocks to execute based on an integral or enumeration expression.

enum class DeviceState {
    Idle,
    Running,
    Error
};

DeviceState state = DeviceState::Running;

switch (state) {
    case DeviceState::Idle:
        std::cout << "Device is idle.\n";
        break;
    case DeviceState::Running:
        std::cout << "Device is running.\n";
        break;
    case DeviceState::Error:
        std::cout << "Device encountered an error!\n";
        break;
    default:
        std::cout << "Unknown state.\n";
        break;
}

Fallthrough and [[fallthrough]] Attribute

By default, if a case block does not end with a break or return, the compiler continues executing the code in the next case block. This is called "fallthrough."

In C++17, if you intentionally want fallthrough behavior, you should mark it with the [[fallthrough]]; attribute to silence compiler warnings:

switch (cmd_flag) {
    case 'v': // verbose
        enable_logging = true;
        [[fallthrough]]; // Informs the compiler this is intentional
    case 's': // start
        start_device();
        break;
    default:
        stop_device();
}

3. Compile-Time Conditionals: if constexpr (C++17)

One of the most powerful features of Modern C++ is if constexpr. It allows conditional execution at compile time. The compiler evaluates the condition, keeps the branch that evaluates to true, and discards the false branch completely from the generated binary.

This is widely used in generic template programming to run different code depending on type properties.

#include <iostream>
#include <type_traits>

template <typename T>
void process_input(T value) {
    if constexpr (std::is_pointer_v<T>) {
        std::cout << "Processing a pointer. Address: " << value 
                  << ", Value: " << *value << "\n";
    } else {
        std::cout << "Processing a direct value: " << value << "\n";
    }
}

int main() {
    int num = 100;
    process_input(num);   // Keeps the 'else' block, discards the 'if'
    process_input(&num);  // Keeps the 'if' block, discards the 'else'
    return 0;
}

If we wrote a standard if here, compiling process_input(num) would fail because the compiler would try to compile *value (dereferencing a non-pointer integer num). if constexpr prevents the inactive branch from being compiled.


4. Loops and Iterations

while and do-while Loops

  • while loops check the condition before executing the block.
  • do-while loops check the condition after executing the block, ensuring the body runs at least once.
int retries = 3;
while (retries > 0) {
    if (connect_to_server()) break;
    retries--;
}

char input;
do {
    std::cout << "Confirm exit (y/n): ";
    std::cin >> input;
} while (input != 'y' && input != 'n');

Traditional for Loop

Ideal when you need to know the index or loop counter.

1
2
3
for (int i = 0; i < 5; ++i) {
    std::cout << "Iteration: " << i << "\n";
}

Range-Based for Loop

Introduced in C++11, this is the modern way to iterate through containers (like std::vector, std::array, and strings). It is cleaner and prevents off-by-one boundary errors.

#include <vector>
#include <string>

std::vector<std::string> messages = {"Init", "Process", "Cleanup"};

// 1. Iteration by Value (Copies each string - slow for large objects)
for (auto msg : messages) {
    std::cout << msg << "\n";
}

// 2. Iteration by Const Reference (Read-only, zero copying - highly recommended)
for (const auto& msg : messages) {
    std::cout << msg << "\n";
}

// 3. Iteration by Non-Const Reference (Allows modifying elements)
for (auto& msg : messages) {
    msg += " [OK]";
}

C++20 Range Loop with Init-statement

Similar to C++17 if statements, C++20 allows you to declare variables inside a range-based for loop, useful for maintaining an index counter:

1
2
3
4
5
6
std::vector<int> readings = {23, 25, 22, 28};

// 'index' is created locally, incremented in the loop, and goes out of scope after the loop
for (int index = 0; int temp : readings) {
    std::cout << "Sensor " << index++ << " read: " << temp << "C\n";
}

5. Jump Statements: break and continue

  • break: Exits the nearest enclosing loop or switch block immediately.
  • continue: Skips the remaining lines in the current iteration of a loop and moves directly to the next loop evaluation.
for (int i = 1; i <= 10; ++i) {
    if (i == 4) {
        continue; // Skip printing 4
    }
    if (i == 8) {
        break; // Exit the loop entirely when reaching 8
    }
    std::cout << i << " ";
}
// Output: 1 2 3 5 6 7

  • Functions — How code organization interacts with control flow.
  • Operators — Comparison operations and short-circuit evaluation rules.