A program that only runs straight through, top to bottom, can't react to anything. Real programs need to make decisions: charge shipping if the cart total is below a threshold, apply a discount if the customer is a member, refuse the order if a product is out of stock. The if statement is the simplest tool C++ provides for that, and this lesson covers how it works, the variations (else, else if, nested, with initializer), and the small set of mistakes that show up often.
if StatementAn if statement runs a block of code only when a condition is true. The shape is:
The condition lives inside the parentheses. The body lives inside the curly braces. If the condition evaluates to true, the body runs. If not, the body is skipped, and execution continues after the closing brace.
The smallest useful example, deciding whether to charge shipping based on the cart total:
The condition cartTotal < 50.0 evaluates to true, so the shipping message prints. Changing cartTotal to 75.0 skips the if body and only the order total line runs. The line after the closing brace runs every time, because it sits outside the if.
The condition can be any expression that produces a value the compiler can convert to bool. A comparison like cartTotal < 50.0 produces a bool directly. So does isMember == true, or stockCount > 0. Other expressions also work, as shown below.
The condition of an if is converted to bool before it's tested. For bool values that's a no-op, but C++ also lets a handful of other types convert. The common rules:
int, short, long, char), 0 converts to false and any non-zero value converts to true.float, double), 0.0 converts to false and any non-zero value converts to true. Very small values near zero still count as true.nullptr or NULL) converts to false. Any non-null pointer converts to true.true is true, false is false.This implicit conversion is why the following two if statements behave the same way:
Both print. The first if converts stockCount (a non-zero int) to true. The second compares it to 0 explicitly. The implicit form is shorter, but the explicit form makes intent obvious. For numeric values that conceptually represent counts, most modern style guides prefer the explicit form (stockCount != 0 or stockCount > 0). For pointers, the short form if (ptr) is conventional and appears everywhere.
Strings are a trap here. A std::string does not implicitly convert to bool. To check whether a string is empty, call .empty():
A C-style string (const char*) is a pointer, so it does convert: a null const char* is false, and any non-null pointer (including one pointing at an empty "" string) is true. That mismatch between const char* and std::string is a known source of confusion.
if with elseAn if on its own only handles one path. To do one thing if the condition is true and a different thing if it's false, add an else:
The else block runs only when the if condition was false. Exactly one of the two blocks runs every time, never both, never neither.
A shipping calculator that picks between two messages:
With cartTotal at 75.0, the condition cartTotal < 50.0 is false, so the else block runs. Changing the total to 42.50 runs the first block instead. The two branches are mutually exclusive.
A common shape is a small validation: if a value is bad, complain; otherwise carry on.
else if ChainsTwo paths cover a lot of cases, but not all. To pick between three or more options, chain else if:
The conditions are checked in order, top to bottom. As soon as one of them is true, that block runs and the rest of the chain is skipped. If none match, the final else block runs (or nothing runs, if there's no else).
A discount tier calculator based on cart total:
cartTotal is 125.0. The first check (>= 200.0) is false. The second check (>= 100.0) is true, so the 10% branch runs and the chain stops there. The compiler doesn't even evaluate the >= 50.0 check, because the chain is already done.
The order of the checks matters when the conditions overlap. Here, 125.0 satisfies both >= 100.0 and >= 50.0, but the higher discount tier should win. Writing the conditions from highest to lowest threshold (or most-specific to least-specific) handles that cleanly. Reversing the order changes the result:
Now a cart of 125.0 gets the 5% tier, because >= 50.0 is already true. The else if for >= 100.0 never runs. The bug produces no compile error, no runtime crash, only the wrong discount. Always think through the order when conditions overlap.
The switch statement is sometimes a cleaner way to handle a chain of comparisons against a single value, but it only works for integer-like types. For range checks like this one, else if is the right tool.
if StatementsAn if can sit inside another if. The inner one only runs when the outer condition is true, which lets two related conditions be checked one after the other.
The outer if checks whether the product is in stock. If it is, the inner if decides which price to show. The else of the outer if handles the out-of-stock case, regardless of membership.
Nesting can go as deep as needed, but readability falls off fast. Three or four levels of nesting is usually a sign that the logic should be flattened, either by combining conditions with && and ||, or by returning early from a function. Compare the version above with this flattened one:
Same behavior, less nesting. Both versions are valid; the second one is easier to scan because each path is one level deep.
Each if body has its own block, and any variable declared inside that block is only visible there. The same scope rule applies to the bodies of if, else if, and else.
discount lives only inside the if body. Reading it after the closing brace is a compile error: the name is no longer in scope.
if Without BracesC++ allows skipping the braces when the if body is a single statement:
That compiles and runs like the braced version. Adding a second statement changes the meaning without warning:
The indentation makes both lines look like they belong to the if, but they don't. Only the first statement is part of the if. The second statement is outside it and runs every time, regardless of the condition. The compiler doesn't care about indentation, only about braces.
This is a famous source of bugs, infamous enough to have caused a real-world security vulnerability (the "goto fail" bug in Apple's SSL code in 2014, which used the same shape of mistake). The rule: always use braces, even for single-statement bodies. It costs two characters and prevents an entire category of errors.
Now both lines clearly belong to the if, and adding a third puts it inside the braces with the rest. Some compilers (recent g++ and clang++ with -Wmisleading-indentation) warn about the un-braced version, but the warning is not guaranteed. Bracing every branch removes the problem.
A few specific mistakes show up often enough to name explicitly.
= vs === is assignment. == is equality comparison. They look similar and mean different things.
The if (stockCount = 5) is almost certainly a typo for if (stockCount == 5). The buggy version assigns 5 to stockCount, then uses the result of the assignment (5) as the condition. Since 5 is non-zero, the if body runs every time, and the variable's value is overwritten.
This is one of the oldest C and C++ bugs. Most modern compilers warn about it (g++ with -Wall produces something like warning: suggest parentheses around assignment used as truth value), but the code still compiles. The fix is the equality operator:
Some developers write the constant on the left side (if (5 == stockCount)) so that a typo becomes a compile error (if (5 = stockCount) won't compile, because a literal can't be assigned to). It's a defensive habit. Modern style guides are split on whether it's worth the awkwardness.
if (...)A semicolon ends a statement. A semicolon right after the parentheses of an if ends the statement with an empty body, and the next block runs unconditionally:
The if (stockCount > 0); is a complete statement with an empty body. The block that follows is a regular block (not attached to the if), and its body runs no matter what. The output prints even though the stock is zero.
The semicolon is invisible in casual reading, which is what makes the bug nasty. Compilers like g++ warn about this with -Wall (warning: this 'if' clause does not guard...), but again, the warning isn't required. Treat the rule "no semicolon after a control-flow header" as a hard habit.
In a chain of ifs without explicit braces, an else always attaches to the nearest unmatched if. Always. Even if the indentation suggests otherwise:
(The program prints nothing.)
The indentation makes it look like the else belongs to the outer if (stockCount > 0), but it actually attaches to the inner if (isMember). When stockCount is 0, the outer if is false, the whole inner construct is skipped, and the else never runs. The "Out of stock" message never prints, even though that's what the author intended.
This is the dangling-else problem, and the fix is the same as for the one-line if: brace every branch. With braces, the structure is unambiguous and the parser has no choice:
Same logic, but now the else clearly belongs to the outer if, and the right message prints. Bracing every branch is one of the few habits in C++ that prevents multiple distinct bugs at once.
An if/else if/else chain is a single decision flow. Each diamond is a condition that's checked in order, and the first one that's true wins.
The diagram traces the discount-tier example from earlier. The check on the cart total flows down through the diamonds in source-code order. As soon as one diamond answers Yes, the corresponding action runs and the rest of the chain is skipped. The final No discount branch represents the else, the catch-all that runs when every preceding condition was false.
That short-circuit behavior is built into the language. The compiler doesn't waste cycles checking >= 50.0 after >= 100.0 already matched. The chain stops at the first match.
if with Initializer (C++17)C++17 added a useful variant: declare and initialize a variable as part of the if header, scoped to the if block:
The variable lives for the whole if/else construct, including the else branch, and is destroyed when the construct ends. This is handy for a value needed only to test and use inside the branch, without polluting the surrounding scope.
A common case is checking whether a map lookup found anything:
stock.find returns an iterator. The traditional pre-C++17 version had to declare it before the if, where it would linger after the if ended, available to be used (or misused) anywhere later in the function. The initializer form keeps it tightly scoped to the if/else, matching the intent: the variable is only needed for the decision.
The same idea works with any return value. For example, opening a file:
The orderFile variable lives for the duration of the if/else. When the construct ends, its destructor runs and the file is closed automatically. Tight scope, automatic cleanup.
if constexprC++17 also introduced if constexpr, which looks similar but does something different. It's a compile-time if: the compiler picks one branch and discards the other entirely, based on a condition that must be known at compile time. It's mostly used in templates, where different code paths are needed for different types.
if constexpr isn't covered here. It's a more advanced feature that follows templates. The regular if covered in this lesson is a runtime decision, evaluated each time the code runs.
A final example that exercises several variations together, the kind of small check that appears in a real ordering function:
The outer if/else if/else checks three different failure modes (not in catalog, out of stock, not enough stock) and falls through to the success branch only when all three are clear. The iterator it is scoped to the if block thanks to the initializer form. Inside the success branch, a small nested if adjusts the total for members. Every branch is braced, so there's no ambiguity about which else belongs to which if, and there's nowhere for a stray semicolon or one-line typo to cause trouble.
10 quizzes