C++ Part I · Module 4 · Expressions and Operators
Logical Operators and Short-Circuit Evaluation
Chapter 4.4 ended with a range check that needed two comparisons joined into one condition. The operator that joined them is the subject here, and it has a property that is easy to mistake for a performance detail. When C++ evaluates an and-expression and the left side is already false, it does not evaluate the right side at all — and when it evaluates an or-expression whose left side is already true, the same thing happens. That sounds like the compiler being efficient. It is not: it is specified behaviour that the standard requires, which makes it something you can build correctness on rather than something you hope for. The difference matters, because a whole category of safety check depends on it. The guard that stops Chapter 4.3's division by zero from ever running works precisely because the division is on the right-hand side of an and, and the language promises it will not be reached.
Foundation14 min readC++Part IFundamentalsOperators
Part I · Module 4 · Chapter 4.5 · Logical Operators and Short-Circuit Evaluation
1. Three Operators
a && b // and — true when both are true
a || b // or — true when at least one is true
!a // not — true when a is falseEach produces a bool, like the comparisons in Chapter 4.4. Each takes operands that are bool or convert to one — and Chapter 3.6 §3 established that any non-zero number converts to true.
They let you build a condition out of several facts:
link_up && !parity_error // up and no error
(1 < voltage) && (voltage < 3) // between 1 and 3
timed_out || error_count > 0 // something went wrongThat second line is Chapter 4.4 §5's corrected range check.
So far, unremarkable. The chapter is about what happens to the right-hand operand.
2. The Right-Hand Side May Never Run
Consider a && b. If a is false, the whole expression is false no matter what b is. C++ uses that: it evaluates a, sees false, and stops. b is never evaluated.
The same for a || b. If a is true, the answer is true regardless of b, so b is never evaluated.
This is called short-circuit evaluation. Here it is measured, using a function with an effect you can see:
#include <iostream>
static int probe_calls = 0;
bool register_ready()
{
++probe_calls;
std::cout << " (register_ready() was called)\n";
return true;
}
int main()
{
bool link_up = false;
std::cout << "link_up && register_ready():\n";
if (link_up && register_ready()) { }
std::cout << " calls so far: " << probe_calls << '\n';
std::cout << "link_up || register_ready():\n";
if (link_up || register_ready()) { }
std::cout << " calls so far: " << probe_calls << '\n';
return 0;
}Expected output:
link_up && register_ready():
calls so far: 0
link_up || register_ready():
(register_ready() was called)
calls so far: 1Read both halves.
With && and a false left side, the function was never called. The counter stayed at 0 and the message never printed.
With || and a false left side, it was called. false || something depends on the something, so the right side had to be evaluated.
The rule in one line:
&&evaluates its right operand only if the left is true.||evaluates its right operand only if the left is false.
3. It Is a Guarantee, Not an Optimisation
This is the part that decides whether you can rely on it.
A reasonable guess is that short-circuiting is the compiler being efficient — skipping work it knows cannot change the answer. If that were true, it would be an optimisation, and optimisations are permitted rather than required. Code that depended on one would be fragile.
It is not an optimisation. The standard requires it. For the built-in && and ||, the left operand is evaluated first, and the right operand is evaluated only in the case described above. A conforming compiler has no choice.
That changes what you may do with it:
- You may put something on the right-hand side that would be wrong to evaluate when the left is false.
- You may rely on the left side acting as a precondition for the right.
- You may count on the left side's side effects happening before the right side's.
Chapter 4.9 covers evaluation order generally, and the headline there is that C++ leaves a great deal of it unspecified. && and || are among the few places where the order is pinned down — which is exactly why they are useful for the next section.
4. The Guard Pattern
The practical payoff. Chapter 4.3 §5 established that integer division by zero is undefined behaviour. Here is how you prevent it:
#include <iostream>
int main()
{
int count = 0;
int total = 50;
if (count != 0 && total / count > 10) {
std::cout << "high average\n";
} else {
std::cout << "guard held — no division happened\n";
}
return 0;
}Expected output:
guard held — no division happenedcount is 0, so count != 0 is false, so the right-hand side — which contains the division — is never evaluated.
The proof that it really did not run is better than the printed message. Chapter 4.3 §5 showed that -fsanitize=undefined detects division by zero precisely. Build this program with it:
$ c++ -std=c++17 -fsanitize=undefined guard.cpp -o guard && ./guard
guard held — no division happenedNo runtime error. The sanitizer that catches division by zero reported nothing, because there was no division to catch.
The shape generalises to any check where one condition makes another safe or meaningful:
if (buffer_valid && buffer[index] == expected) // check before use
if (count > 0 && total / count > threshold) // check before divide
if (!timed_out && response_code == 0) // check before trustThe order is load-bearing. Swap the two operands in any of those and the protection is gone, because the thing you were guarding against now runs first.
5. When the Right-Hand Side Has a Side Effect
Short-circuiting is a guarantee you can rely on, which means it is also a trap if you were relying on something else.
if (link_up && record_transaction()) { /* ... */ }If record_transaction() both returns a bool and updates a count, that count is only updated when link_up is true. Depending on what the function is for, that is either exactly right or a bug that appears only on the failing path — which is the worst place for a bug to hide.
The fix is to separate the effect from the condition:
const bool recorded = record_transaction(); // always runs
if (link_up && recorded) { /* ... */ }The general advice is stronger than this one case: prefer conditions whose operands have no side effects. A condition should ask a question, not change the program. When you need both, do the changing on its own line where it is visible.
6. ! and Readability
! inverts:
!link_up // true when link_up is false
!(a && b) // true unless both are trueTwo practical notes.
Parentheses matter with !. It binds very tightly — tighter than the comparisons, the logical operators and the bitwise operators from Chapter 4.8. That last one produces a genuine trap:
const std::uint32_t flags = 0x20u; // bit 5 set
const std::uint32_t MASK = 0x10u; // we are asking about bit 4
!flags & MASK // means (!flags) & MASK → false
!(flags & MASK) // "bit 4 is clear" → trueThe first negates flags as a whole and then masks the 0 or 1 that produces; the second asks the question that was intended. Clang warns about exactly this one:
warning: logical not is only applied to the left hand side of this bitwise
operator [-Wlogical-not-parentheses]Worth being precise about the general case: for two bool operands, (!a) == b and !(a == b) happen to be equivalent, so the comparison version of this trap is usually harmless. It is when ! meets a bitwise operator, or an operand that is not already a truth value, that the grouping changes the answer. Chapter 4.9 covers precedence properly; until then, parenthesise anything that is not obvious.
Prefer the positive form when you have a choice. if (!(count == 0)) is harder to read than if (count != 0), and they are the same question. A condition that needs two mental steps to understand is where review misses things.
7. && Is Not &
A real source of confusion, and worth settling before the next chapter introduces the other one.
| Operands | Result | Short-circuits? | |
|---|---|---|---|
&& | logical values | a bool | Yes |
& | bit patterns | a value with bits combined | No |
&& asks "are both of these true?". & combines two values bit by bit, and Chapter 4.8 is about it.
Writing & where you meant && frequently still compiles, because the result converts to bool. It then behaves differently in two ways: both operands are always evaluated, so your guard is gone, and for values other than 0 and 1 the bitwise result can be 0 when both operands were "true". 1 & 2 is 0, so a condition written with & can be false when both sides were non-zero.
The same applies to | and ||.
Logical question, logical operator. If you are asking whether something is true, use
&&and||.
8. Mistakes Worth Making Once
Treating short-circuiting as an optimisation. It is required behaviour. You may build correctness on it.
Putting the guard second. total / count > 10 && count != 0 divides before checking. Order is the whole mechanism.
Generalising the guarantee. Only && and || sequence and skip. f() + g() promises nothing — Chapter 4.9.
Hiding a side effect on the right-hand side. It then happens only sometimes, and only on the path you tested least.
Using & for a logical question. Both operands always run, and 1 & 2 is 0 even though both are "true".
Writing !flags & MASK. ! binds tighter than &, so this negates flags and then masks the result. Clang warns with -Wlogical-not-parentheses.
Preferring !(x == y) over x != y. Same meaning, more effort to read.
9. What You Should Be Able to Say Now
&&,||and!produce abooland accept anything that converts to one.&&evaluates its right operand only when the left is true;||only when the left is false.- Measured: with a false left operand, the right-hand function was never called under
&&and was called under||. - This is a guarantee in the standard, not an optimisation, so you may rely on it for correctness.
- It therefore lets you put something on the right that would be invalid to evaluate when the left is false.
- The guard pattern:
count != 0 && total / count > 10. Measured with-fsanitize=undefined— no division-by-zero report, because no division happened. - The order of the operands is load-bearing; swapping them removes the protection.
- Do not generalise the sequencing to other operators.
f() + g()gives no such promise — Chapter 4.9. - A side effect on the right-hand side happens only sometimes. Move it to its own line.
- Prefer conditions that ask a question rather than change the program.
!binds tighter than the comparisons, the logical operators and the bitwise operators. Measured:!flags & MASKis false where!(flags & MASK)is true. For twobooloperands the comparison version is harmless; with bitwise operators it is not.&&is not&. The bitwise form evaluates both operands and can give 0 for two non-zero values.
10. Exercises
Exercise 1 — count the calls. probe() prints a line and returns true. For each, say how many times it prints: (a) false && probe() (b) true && probe() (c) false || probe() (d) true || probe().
Exercise 2 — explain why the guard is safe. In count != 0 && total / count > 10, explain what would happen if C++ evaluated both operands always, and name the chapter that established the consequence.
Exercise 3 — break it. Rewrite that guard so it no longer protects anything, changing nothing but the order. Say what the program now does when count is 0.
Exercise 4 — find the hidden effect. if (ready && log_and_check()) — the function appends to a log and returns a result. Describe the behaviour when ready is false, say why this is hard to notice in testing, and give the fix.
Exercise 5 — & versus &&. Show two distinct ways if (a & b) can behave differently from if (a && b). One should be about evaluation, the other about the result.
Exercise 6 — parenthesise. Say what !flags & MASK means as written, what the author almost certainly intended, and how to write it. Then explain why the same mistake with two bool operands and == would not have changed the answer.
Exercise 7 — order a real check. You must test that a pointer-free index is in range and that the element at it matches. Given valid_count, index and a function element_at(index), write the condition with the operands in an order that is safe, and say what makes the order correct.
11. Summary
&&,||and!produce abooland take anything convertible to one.- Short-circuit:
&&skips its right operand when the left is false;||skips it when the left is true. - Measured: the right-hand function ran 0 times under
&&with a false left side, and once under||. - It is a standard guarantee, not an optimisation — which is what makes guards correct rather than lucky.
- The guard pattern works: with
-fsanitize=undefined, the division-by-zero check reported nothing because the division never ran. - Operand order is the mechanism. Swap it and the protection disappears.
- Do not generalise — only
&&and||sequence and skip. Chapter 4.9 has the general rules. - Keep side effects out of conditions; a condition should ask, not act.
&&is not&— the bitwise form always evaluates both and can give 0 for two non-zero operands.!binds tighter than&—!flags & MASKis not!(flags & MASK), and the compiler warns.
That last distinction is the bridge to the next chapter. & has been mentioned three times now as the operator that works on bit patterns rather than on truth, and deferred each time. It is the tool Chapter 3.4 §5 said was needed to get a sub-field in and out of a register and could not teach, because the operators had not been covered. Chapter 4.8 covers them — but two smaller operator groups come first, and the next one is the one you have used in every program since Chapter 2.2 without noticing it is an operator at all.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
