Control the Flow of Your C++ Programs
Learn to use if/else, loops, and switch to make your C++ programs make decisions and repeat work automatically.
By the end of this page, you will be able to write programs that branch based on conditions using if/else, repeat work using for and while loops, choose between many cases using switch, and avoid the most common mistakes beginners make with control flow.
What and Why
Every useful program makes decisions. A thermostat turns the heater on if the room is cold. A download manager retries while the connection is unstable. A music player skips to the next track when the current one ends.
In C++, control flow is the set of statements that tell the CPU which instructions to execute next, and how many times. Without it, your program is just a straight line β it runs every statement once, top to bottom, and exits. Control flow lets you build programs that react, repeat, and choose.
C++11 is the baseline standard for this page. Everything here compiles with -std=c++11 or newer.
Step by Step
Branching with if and else
The simplest decision: run one block of code if a condition is true, and a different block if it is false.
#include <iostream>
int main() {
int temperature = 18;
if (temperature < 20) {
std::cout << "It's cold β wear a jacket.\n";
} else {
std::cout << "Nice weather!\n";
}
return 0;
}The condition inside if (...) must evaluate to a boolean β true or false. Any non-zero integer counts as true; zero counts as false.
You can chain more conditions with else if:
#include <iostream>
int main() {
int score = 74;
if (score >= 90) {
std::cout << "Grade: A\n";
} else if (score >= 80) {
std::cout << "Grade: B\n";
} else if (score >= 70) {
std::cout << "Grade: C\n";
} else {
std::cout << "Grade: F\n";
}
return 0;
}C++ evaluates the conditions top to bottom and runs the first block whose condition is true. The rest are skipped.
Repeating work with for
A for loop runs a block of code a specific number of times.
#include <iostream>
int main() {
for (int i = 0; i < 5; i++) {
std::cout << "Count: " << i << "\n";
}
return 0;
}The three parts inside for(...) are:
| Part | What it does |
|---|---|
int i = 0 | Initialise a counter before the first iteration |
i < 5 | Check this before each iteration; stop when false |
i++ | Run this after each iteration (increment the counter) |
C++11 added a range-based for, which is cleaner when you want to visit every element in a collection:
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores = {88, 95, 72, 61};
for (int s : scores) {
std::cout << "Score: " << s << "\n";
}
return 0;
}Read for (int s : scores) as "for each int s in scores". No index arithmetic needed.
Repeating while a condition holds: while
Use while when you do not know in advance how many times you need to loop.
#include <iostream>
int main() {
int n = 1;
while (n <= 100) {
n *= 2;
}
std::cout << "First power of 2 above 100: " << n << "\n";
return 0;
}The loop body runs as long as the condition is true. If the condition is false the very first time, the body never runs at all.
A do/while loop guarantees the body runs at least once β the condition is checked at the end:
#include <iostream>
int main() {
int guess;
do {
std::cout << "Guess a number (1β10): ";
std::cin >> guess;
} while (guess < 1 || guess > 10);
std::cout << "Valid input received.\n";
return 0;
}Choosing between many cases: switch
When you have one integer or character value and many possible outcomes, switch is cleaner than a chain of else if.
#include <iostream>
int main() {
int day = 3;
switch (day) {
case 1:
std::cout << "Monday\n";
break;
case 2:
std::cout << "Tuesday\n";
break;
case 3:
std::cout << "Wednesday\n";
break;
default:
std::cout << "Another day\n";
break;
}
return 0;
}The break at the end of each case is required β without it, execution falls through to the next case (more on this below).
Common Patterns
Early exit with break and continue
break exits the nearest enclosing loop immediately. continue skips the rest of the current iteration and goes straight to the next check.
#include <iostream>
int main() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) continue; // skip even numbers
if (i > 10) break; // stop after 10
std::cout << i << " ";
}
std::cout << "\n";
// Output: 1 3 5 7 9
return 0;
}Searching a collection
#include <iostream>
#include <vector>
#include <string>
int main() {
std::vector<std::string> names = {"Alice", "Bob", "Carol", "Dave"};
std::string target = "Carol";
bool found = false;
for (const std::string& name : names) {
if (name == target) {
found = true;
break;
}
}
if (found) {
std::cout << target << " is in the list.\n";
} else {
std::cout << target << " was not found.\n";
}
return 0;
}Accumulating a result
#include <iostream>
#include <vector>
int main() {
std::vector<int> prices = {12, 7, 34, 5, 19};
int total = 0;
for (int p : prices) {
total += p;
}
std::cout << "Total: " << total << "\n";
return 0;
}What Can Go Wrong
Forgetting break in a switch
// BUG: falls through from case 1 into case 2
switch (x) {
case 1:
std::cout << "One\n";
// missing break!
case 2:
std::cout << "Two\n";
break;
}
// If x == 1, prints both "One" and "Two".Always add break at the end of every case unless fall-through is intentional and clearly commented.
Using = instead of == inside if
int x = 5;
if (x = 10) { // BUG: assigns 10 to x, always true
std::cout << "x is 10\n";
}= assigns; == compares. The compiler may warn about this β read your warnings. A defensive habit is to put the constant on the left: if (10 == x) β an accidental = then becomes a compile error.
Off-by-one errors in for loops
// Intends to print indices 1 through 5
for (int i = 1; i <= 5; i++) { // correct: <=
std::cout << i << " ";
}
// Common mistake: stops at 4
for (int i = 1; i < 5; i++) { // wrong: < misses the last element
std::cout << i << " ";
}Read the condition carefully: < vs <= changes the last iteration.
Infinite loops
int i = 0;
while (i < 10) {
std::cout << i << "\n";
// BUG: forgot i++, loop never ends
}If your program hangs, check that every loop variable gets updated and can eventually make the condition false.
Quick Reference
| Statement | Use when |
|---|---|
if / else if / else | You have a condition that is true or false |
switch | One integer/char value, many fixed cases |
for (init; cond; step) | You know the iteration count in advance |
for (T x : collection) | You want every element of a range (C++11) |
while (cond) | Repeat while a condition holds; may run zero times |
do { } while (cond) | Like while, but body runs at least once |
break | Exit the current loop or switch immediately |
continue | Skip to the next loop iteration |
Early Return and the Guard Clause Pattern
A guard clause is an early return (or throw) at the top of a function that rejects invalid input immediately, before any real work begins.
The alternative β wrapping everything inside nested if blocks β pushes the actual logic to deeply indented levels that are harder to read and test. Guard clauses keep the happy path at the top level of indentation where it is easy to follow.
#include <iostream>
#include <string>
// Deeply nested style β hard to follow
double divide_nested(double a, double b) {
if (b != 0) {
if (a >= 0) {
return a / b;
} else {
return 0; // negative numerator not allowed in this domain
}
} else {
return 0; // cannot divide by zero
}
}
// Guard clause style β happy path is obvious
double divide_guarded(double a, double b) {
if (b == 0) return 0; // guard: reject zero divisor
if (a < 0) return 0; // guard: reject negative numerator
return a / b; // happy path β clear and unindented
}
int main() {
std::cout << divide_guarded(10.0, 2.0) << "\n"; // 5
std::cout << divide_guarded(10.0, 0.0) << "\n"; // 0
}The "single exit point" philosophy (one return per function) was popular in C because it made resource cleanup easier to manage. In modern C++ that problem is solved by destructors and RAII, so the single-exit rule no longer applies β prefer guard clauses for clarity.
goto β When It Actually Makes Sense
goto transfers execution to a labeled statement anywhere in the same function, bypassing the normal top-to-bottom flow.
In almost every case it produces code that is harder to follow than an equivalent loop or function call. The one context where you will legitimately encounter it is legacy C-style resource cleanup β when a function opens several resources in sequence and must close all of them on any error path.
#include <cstdio>
// C-style resource management using goto for cleanup
int process_files(const char* input_path, const char* output_path) {
FILE* in = nullptr;
FILE* out = nullptr;
int result = -1;
in = fopen(input_path, "r");
if (!in) goto cleanup;
out = fopen(output_path, "w");
if (!out) goto cleanup;
// ... do actual work ...
result = 0;
cleanup:
if (out) fclose(out);
if (in) fclose(in);
return result;
}In modern C++ you would use RAII wrappers (e.g., std::unique_ptr with a custom deleter, or std::fstream) so that resources close automatically on scope exit β making goto unnecessary. Avoid goto in new code; understand it so you can read old code.
Exception-Based Control Flow
throw transfers control to the nearest matching catch block, unwinding the call stack on the way β this is a fundamentally different kind of branching that crosses function boundaries.
The primary purpose of exceptions is signalling errors that cannot be handled locally. They are not a substitute for ordinary if/else, but understanding them as a control flow mechanism helps you reason about what happens when something goes wrong.
#include <iostream>
#include <stdexcept>
double safe_divide(double a, double b) {
if (b == 0) {
throw std::invalid_argument("division by zero"); // throw jumps out
}
return a / b; // only reached when b != 0
}
int main() {
try {
std::cout << safe_divide(10.0, 2.0) << "\n"; // 5
std::cout << safe_divide(10.0, 0.0) << "\n"; // throws β skips this line
std::cout << "This line is never reached.\n";
}
catch (const std::invalid_argument& e) {
// Control resumes here when a matching exception is thrown
std::cout << "Error: " << e.what() << "\n";
}
std::cout << "Program continues after the try/catch.\n";
}throw inside a try block immediately jumps to the matching catch. Any local variables created between throw and the end of the block are destroyed in reverse order (stack unwinding). A full treatment of exception handling appears in the exception handling guide; for now just remember: exceptions are for errors, not for ordinary flow.
Branch Prediction Hints: [[likely]] and [[unlikely]]
C++20's [[likely]] and [[unlikely]] attributes hint to the compiler which branch of an if or switch is taken most often, allowing it to arrange machine code for the common case.
Modern CPUs predict branches and prefetch instructions speculatively. If the CPU's prediction is wrong, it discards its work and reloads β a measurable cost in tight loops. These attributes let you override the default prediction when you have profile data.
#include <iostream>
int process(int value) {
// We expect value to be positive the vast majority of the time.
if (value > 0) [[likely]] {
return value * 2;
}
// Negative or zero is the unusual case.
if (value < 0) [[unlikely]] {
return -1; // error sentinel
}
return 0;
}
// In a switch, mark the hot case:
void handle_event(int event_type) {
switch (event_type) {
[[likely]] case 1: /* handle common event */ break;
[[unlikely]] case 99: /* handle rare event */ break;
default: break;
}
}
int main() {
std::cout << process(5) << "\n"; // 10
std::cout << process(-3) << "\n"; // -1
}These attributes are hints β the compiler is free to ignore them. Do not add them speculatively; reach for them only after profiling shows a branch is genuinely hot. Requires -std=c++20.
Compile-Time Branching with if constexpr
if constexpr is evaluated entirely at compile time β the branch that is not taken is not instantiated, not type-checked (in most contexts), and generates no code.
This matters most in template code where different types need different logic paths. Unlike a regular if, an if constexpr branch that does not apply can reference members or functions that do not exist on the other type β the compiler just discards it.
#include <iostream>
#include <type_traits>
template <typename T>
void describe(T value) {
// Only one branch is compiled for any given T.
if constexpr (std::is_integral_v<T>) {
std::cout << value << " is an integer\n";
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << value << " is a floating-point number\n";
} else {
std::cout << "unknown type\n";
}
}
int main() {
describe(42); // "42 is an integer"
describe(3.14); // "3.14 is a floating-point number"
describe("hello"); // "unknown type"
}This is a preview of template programming β the concept is mentioned here because it looks like control flow but is resolved before the program runs. Regular if constexpr in non-template functions also exists: if the condition is a compile-time constant expression, the compiler eliminates the dead branch, which can help it produce cleaner code. Full coverage is in the templates guide. Requires -std=c++17.
What's Next
- Variables and Types β understand the data that your conditions and loops operate on
- Functions β package repeated logic into reusable blocks
- C++11 Features β the full set of language improvements in the standard this page targets