TRT
Sign in

Foundations · Module 2 · Lesson 2 of 10

Boolean Logic and Short-Circuit Evaluation

Combine conditions with &&, || and !, learn the precedence and De Morgan rules, and see how short-circuit evaluation is built from jumps and why operand order is part of your program's logic.

FreeAbout 7 minutes

By the end you can

  • Evaluate a compound condition using the precedence of !, && and ||
  • Predict which operands of && and || are evaluated and which are skipped
  • Rewrite a negated condition using De Morgan's laws
  • Recognize the classic traps: chained comparisons, x == 1 || 2 and & instead of &&

Builds on Conditions and Branches: if, else and switch.

01 · A question first

The condition count != 0 && total / count > 5 divides by a variable that might be zero, yet it never crashes. What stops the division from running, and is that a clever optimization or a promise written into the language?

Make a guess, then step through the trace below to check it.

02 · Watch it run

A division that never happensstep 1 of 5
int count = 0;int total = 20;if (count != 0 && total / count > 5) {    total = total - 5;}
  1. part 0count != 0: ?
  2. part 1total / count: ?
  3. part 2> 5: ?
  4. part 3whole: ?

The condition has two halves joined by &&. The four cells hold the left test, the division, the comparison with 5, and the result of the whole condition. If the division ran while count is 0, the program would crash.

Machine work: evaluation goes left to right; the && may decide after the left side

03 · The idea

Start with the problem

Real decisions combine facts: the user is logged in and has permission, the file is missing or empty, the value lies between 1 and 100. And sometimes one fact has to be checked before another is even safe to ask. Look at this condition:

cpp
if (count != 0 && total / count > 5) {
    total = total - 5;
}

When count is 0 the division would be a disaster: dividing an integer by zero is undefined behavior, and on common hardware it crashes the program. Yet this code is safe. Why?

How the idea works

The three operators. The symbol && means and, || means or, and ! means not. The results of the first two are:

text
a      b      a && b   a || b
false  false  false    false
false  true   false    true
true   false  false    true
true   true   true     true

The operator !a flips the value of a.

Precedence. The operator ! binds tightest, then the comparison operators, then &&, then ||. So a || b && c means a || (b && c). When you mix them, add parentheses even where they are not required. Readers and compilers (through -Wparentheses) both appreciate it.

Short-circuit evaluation. The left operand of && or || is always evaluated first, completely, side effects included. The right operand is evaluated only if the left did not already decide the answer:

  • For a && b, if a is false the result must be false, so b is skipped.
  • For a || b, if a is true the result must be true, so b is skipped.

This rule makes the opening example safe. The trace follows it with count equal to 0: the left side is false, the right side with the division is never executed (step 3), and the program carries on.

It is jumps again. No special mechanism is involved, as the cpu lines of the trace show. The expression a && b compiles to a comparison and a conditional jump for a that goes straight to the false case if a fails, followed by a second comparison and jump for b. This is lesson 1 once per operand, and skipping the right side means nothing more than jumping past its instructions.

Bitwise operators are different. The operators & and | work on the individual bits of integers and always evaluate both operands. The expression 6 & 3 is 2, whereas 6 && 3 is true.

De Morgan's laws. Two identities let you rewrite negated conditions:

  • !(a && b) equals !a || !b
  • !(a || b) equals !a && !b

For example, "not (adult and has an ID)" means "under 18, or no ID". Pushing the ! inward also flips comparisons: !(age >= 18) is age < 18.

Why it is correct

Short-circuiting is not an optimization that a compiler may or may not apply. It is a guarantee written into the language. The standard says that the right operand of && and || is evaluated only when needed, and that everything on the left, side effects included, is finished first. That is what makes guards reliable. Most other operators promise nothing like it: in f() + g() either function may be called first, and both are always called. The guaranteed ordering and skipping belong to &&, ||, the conditional operator ?: and the comma operator.

De Morgan's laws hold because of the truth table. The expression a && b is false exactly when at least one operand is false, which is exactly when !a || !b is true. You can verify all four rows by hand.

What it costs

Short-circuiting saves work whenever the left side is cheap and often decisive. If the right side is a function call that takes 1 microsecond, and the left side rules it out 90 percent of the time, a million evaluations save about 0.9 seconds. So put the cheap, usually decisive condition first, as long as it does not change the meaning: a guard must always stay on the left.

Each operand is also a branch, with the prediction costs that lesson 1 described. In hot loops, compilers sometimes merge simple conditions into branchless code. You rarely need to optimize this by hand, and readability should win.

Pitfalls and variations

Side effects on the right. In if (ok && counter++ > 3), the counter only increments when ok is true. Keep conditions free of side effects, so that skipping can never change the state of the program.

The or-with-a-number trap. The condition x == 1 || 2 means (x == 1) || 2. Since 2 is nonzero, it counts as true, so the condition is always true. Write x == 1 || x == 2.

Chained comparisons. The expression 0 < x < 10 compiles but means (0 < x) < 10: a bool, which is 0 or 1, compared with 10, and always true. Write 0 < x && x < 10. GCC warns about the mistake with -Wall.

Mixing && and ||. Without parentheses, a || b && c groups as a || (b && c), which may not be what you meant. Add the parentheses.

One ampersand. Writing a & b where you meant a && b evaluates both sides and loses the guard. With count != 0 & total / count > 5 the division runs anyway.

Comparing with true. The test if (flag == true) is redundant and fragile when flag is a number. Write if (flag).

Variations. C++ also accepts the words and, or and not as spellings of &&, || and !. A user-defined type can overload && and ||, but the overloaded versions evaluate both operands and lose short-circuiting, which is why library authors avoid overloading them.

Where it is used

You will see guards in input validation, range checks such as low <= x && x <= high, and loops like while (i < size && data[i] != key), which you will write once arrays arrive. The shell uses the same idea: in make && ./run, the second command runs only if the first returned exit status 0, the convention from module 1.

The next lesson, "Loops: while, do-while and for", repeats a block of code by jumping backwards, and the same conditions you combined decide when the repetition stops.

04 · Prove it

Question 1 of 4 · answer from memory

What is the value of `true || false && false`?

05 · Practice

Something wrong with this lesson? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save