C++ / OPERATORS AND EXPRESSIONS
Logical operators and short-circuit evaluation
Predict short-circuit evaluation in C++ and order &&/|| operands so unsafe operations are guarded and needed side effects are never skipped.
What you will learn
- Predict which operand of && or || is skipped and why the left one always runs first.
- Guard a dereference or a division by putting the test to the left of &&.
- Know that &&, || and ! yield bool in C++, never the operand's own value.
- Spot when a single & or | silently removes short-circuiting from a condition.
Understanding Logical operators and short-circuit evaluation
The three logical operators are &&, || and !. Each converts its operands to bool first and produces a bool, so 0 || 7 is true rather than 7, and !5 is false. Any type with a conversion to bool can be an operand: an int, a raw pointer, a stream, a std::optional. Because the result is always a verdict and never one of the operands, C++ has no value-selecting or-idiom; when you want a value instead of a truth test, use the conditional operator a ? a : b.
For the built-in operators, && and || are among the few in C++ with a guaranteed evaluation order. The left operand is fully evaluated first, including all of its side effects, and only then, if the answer is still undecided, is the right operand evaluated at all. A false left operand of && already fixes the result at false, so the right side is never touched; a true left operand of || does the same for true. The mental model that fits is an if statement folded into an expression: a && b means evaluate a, and only if it was true, evaluate b.
This turns operand order into a correctness property rather than a matter of taste. The condition p != nullptr && p->size > 0 is safe because the member access can only run after the null test succeeded, while the same two tests swapped is undefined behaviour. The flip side is that work the program must always perform does not belong on the right of && or ||, since the language is entitled to never run it.
<iostream>
// Prints when it runs, so we can see which operands are evaluated.
bool check(const char* name, bool value) {
std::cout << " ran " << name << '\n';
return value;
}
int main() {
std::cout << std::boolalpha;
std::cout << "false && true\n";
bool a = check("left", false) && check("right", true);
std::cout << " = " << a << "\n\n";
std::cout << "true || false\n";
bool b = check("left", true) || check("right", false);
std::cout << " = " << b << "\n\n";
std::cout << "true && false\n";
bool c = check("left", true) && check("right", false);
std::cout << " = " << c << '\n';
}
The built-in && and || evaluate their left operand first and skip the right one completely once the result is decided, so the order you write the operands in is part of the program's correctness.
Worked examples
Chained pointer guards
Shows how each && test makes the following ones legal, and why reversing them breaks.
<iostream>
struct Node {
int value;
Node* next;
};
bool next_is_positive(const Node* n) {
return n != nullptr && n->next != nullptr && n->next->value > 0;
}
int main() {
Node b{7, nullptr};
Node a{1, &b};
std::cout << std::boolalpha;
std::cout << next_is_positive(&a) << '\n';
std::cout << next_is_positive(&b) << '\n';
std::cout << next_is_positive(nullptr) << '\n';
}
Example explained
Line 1For &a all three operands run: n is non-null, n->next points at b, and b.value is 7.
Line 2For &b evaluation stops at n->next != nullptr, so n->next->value is never read.
Line 3For nullptr only the first test runs; the two member accesses to its right are skipped.
Line 4Writing n->next->value > 0 && n != nullptr would compile and would be undefined behaviour, because && protects only what stands to its right.
&& versus & in a condition
Counts how many operands are actually evaluated when the logical operator is replaced by the bitwise one.
<iostream>
int main() {
std::cout << std::boolalpha;
int calls = 0;
auto probe = [&calls](bool v) { ++calls; return v; };
bool logical = probe(false) && probe(true);
std::cout << "&& : " << logical << ", calls = " << calls << '\n';
calls = 0;
bool bitwise = probe(false) & probe(true);
std::cout << "& : " << bitwise << ", calls = " << calls << '\n';
}
Example explained
Line 1probe increments a captured counter, so calls records how many operands really ran.
Line 2probe(false) && probe(true) stops after the first call, leaving calls at 1.
Line 3probe(false) & probe(true) needs both bool values before it can compute 0 & 1, so calls reaches 2.
Line 4Both lines report false: the answer is identical, but only the && version guarantees the second call is skipped.
The result is a bool, not an operand
Demonstrates that || and ! produce true or false rather than the value that decided the outcome.
<iostream>
int main() {
int chosen = 0;
int fallback = 7;
std::cout << std::boolalpha;
std::cout << "chosen || fallback = " << (chosen || fallback) << '\n';
std::cout << "!chosen = " << !chosen << '\n';
std::cout << "!fallback = " << !fallback << '\n';
int value = chosen ? chosen : fallback;
std::cout << "chosen ? chosen : fallback = " << value << '\n';
}
Example explained
Line 1chosen || fallback converts both ints to bool and yields the bool true, discarding the 7.
Line 2!chosen negates the conversion 0 to false, so it prints true; !fallback prints false because 7 converts to true.
Line 3The conditional operator is what actually selects a value, which is why value holds 7.
Line 4std::boolalpha only changes how bools print, so value still appears as the number 7.
A required call placed on the wrong side
Shows a side effect being silently skipped because the left operand already decided the result.
<iostream>
int loads = 0;
bool load() {
++loads;
return true;
}
int main() {
bool cached = true;
if (cached || load()) {
std::cout << "ready, loads = " << loads << '\n';
}
load();
std::cout << "after explicit call, loads = " << loads << '\n';
}
Example explained
Line 1cached is true, so || fixes the condition at true and load() is never called: loads stays 0.
Line 2The branch still runs, which is why this bug looks like a caching problem instead of a missing call.
Line 3Calling load() as its own statement makes the side effect unconditional and loads becomes 1.
Important notes
&& binds tighter than ||, so a || b && c means a || (b && c); when a is true neither b nor c is evaluated.
Short-circuiting belongs to the built-in operators only. A class that overloads operator&& or operator|| turns the expression into an ordinary function call, so both arguments are evaluated, in an unspecified order.
Common mistakes
Writing if (p->ready && p != nullptr): the guard only covers operands to its right, so the null pointer is dereferenced first and the program has undefined behaviour.
Writing if (x == 1 || 2) instead of if (x == 1 || x == 2): the literal 2 converts to true, so the condition is always true and the branch runs for every x.
Putting a needed call on the right, as in if (cached || load()): once cached is true load() never runs, and the missing work shows up much later as stale data.
Try it yourself
Change, predict, then run
Write bool ok(const int* p) that returns true only when p is not null and *p is a multiple of 3, using a helper bool divisible(int v) that prints "checked" before returning. Call ok(nullptr) and ok(&nine) with int nine = 9, and confirm "checked" appears exactly once.
Open the C++ workspaceCheck your understanding
A condition reads if (p != nullptr && p->ready). Someone rewrites it as if (p != nullptr & p->ready), arguing that both operands are bools so the operators are interchangeable. What happens at run time when p is null?
- Nothing changes, because & and && behave the same way on bool operands.
- It fails to compile, because & rejects bool operands.
- p->ready is still evaluated, the null pointer is dereferenced, and the program has undefined behaviour.
- p->ready is evaluated but its result is discarded, so the guard still holds.
Show answer
& is an arithmetic operator: it must have both operand values before it can compute anything, so p->ready runs even when p is null, and that dereference is undefined behaviour. Option 0 is tempting because whenever both sides are safe to evaluate the resulting bool really is identical, but the thing that makes the guard work is the evaluation that never happens, not the value that comes out.