Boolean Logic with Logical Operators
Combine conditions using &&, ||, and ! with short-circuit evaluation.
What Are Logical Operators?
A relational operator answers exactly one question about exactly one pair of values. Real conditions are rarely that small. A greenhouse controller opens its roof vent only when the air is still and the humidity is low. It raises an alarm when the battery is flat or the radio link is down. Each of those sentences is two separate yes/no questions joined by a word that decides how the answers combine.
Logical operators are that joining word, written as punctuation. C++ provides three of them, and between them they cover every way you can combine or invert a bool.
| Operator | Symbol | Operands | Reads as |
|---|---|---|---|
| Logical NOT | ! |
one | "the opposite of" |
| Logical AND | && |
two | "both of these hold" |
| Logical OR | || |
two | "at least one of these holds" |
Each operand is converted to bool before the operator looks at it, and each of the three produces a bool result. That means logical operators compose freely: the result of one can be an operand of the next, which is how a condition of any size gets built.
The Three Operators at a Glance
Logical NOT is the small one. It takes a single operand and hands back the other bool: !true is false, and !false is true. There is nothing else to it.
The two binary operators fit in a single table:
| Left operand | Right operand | left && right |
left || right |
|---|---|---|---|
| false | false | false | false |
| false | true | false | true |
| true | false | false | true |
| true | true | true | true |
Reading down the two result columns, && yields true in exactly one row, and || yields false in exactly one row. You do not have to memorise eight cells to use them, though. Two sentences carry the whole table:
&&givesfalsethe moment it meets afalseoperand, andtrueonly if it never meets one.||givestruethe moment it meets atrueoperand, andfalseonly if it never meets one.
Almost everything else in this lesson follows from taking those two sentences literally.
How && and || Decide
Most binary operators in C++ leave the order in which their operands are evaluated up to the compiler. Logical AND and logical OR are the exception. The standard requires that the left operand is fully evaluated first, and only then does the operator decide what to do next:
- Evaluate the left operand and convert the result to
bool. - Look at that value. For
&&, afalseon the left settles the answer atfalse. For||, atrueon the left settles the answer attrue. - If the answer is settled, stop. The right operand is never evaluated at all.
- Otherwise, evaluate the right operand. Its value becomes the answer.
Step 3 has a name: short-circuit evaluation. The operator stops working the instant the result is no longer in doubt.
Short-circuiting is not the compiler being clever behind your back. It is written into the language rules, along with the guarantee that the left operand goes first. You can depend on both.
Watching an Operand Get Skipped
Short-circuiting is easy to state and easier to believe once you have watched it happen. Because the left operand is guaranteed to be evaluated first, a pair of functions that announce themselves makes the behaviour visible in a completely predictable order:
#include <iostream>
bool isCalm(int windSpeed)
{
std::cout << " reading the anemometer\n";
return windSpeed < 18;
}
bool isDry(int humidity)
{
std::cout << " reading the hygrometer\n";
return humidity < 55;
}
int main()
{
std::cout << "Gusty morning:\n";
if (isCalm(34) && isDry(41))
std::cout << " vent opens\n";
else
std::cout << " vent stays shut\n";
std::cout << "Still morning:\n";
if (isCalm(9) && isDry(41))
std::cout << " vent opens\n";
else
std::cout << " vent stays shut\n";
return 0;
}
Gusty morning:
reading the anemometer
vent stays shut
Still morning:
reading the anemometer
reading the hygrometer
vent opens
The hygrometer is read once, not twice. On the gusty morning isCalm(34) returned false, the && had its answer, and isDry was never called. Logical OR behaves the same way with the roles reversed: a true on the left ends the expression there.
Short-Circuiting as a Guard
Skipping work is a small speed win. The more valuable use is skipping work that would be invalid, and this is the pattern to recognise:
#include <iostream>
int main()
{
int sampleCount{0};
int rainfallTotal{0};
if (sampleCount != 0 && rainfallTotal / sampleCount > 12)
std::cout << "Average rainfall is high\n";
else
std::cout << "No high-rainfall reading\n";
return 0;
}
No high-rainfall reading
The division on the right would be a division by zero, which is undefined behaviour. It never runs, because sampleCount != 0 is false and && stops there. Written the other way round, with the division first, the same program would be broken. The left operand of && is where a precondition belongs.
Side Effects Behind the Circuit
The flip side is that any operand which does something, rather than merely computing a value, may silently not do it. Recall from the increment and decrement lesson that ++retryCount has a side effect: it changes the variable. Put that on the right of an && and the change becomes conditional on something that has nothing to do with retries:
#include <iostream>
int main()
{
int stationId{6};
int retryCount{0};
if (stationId == 3 && ++retryCount == 1)
std::cout << "Station 3 was retried\n";
std::cout << "retryCount is now " << retryCount << '\n';
return 0;
}
retryCount is now 0
The counter never moved. stationId is 6, so the left comparison was false, and ++retryCount was one of the operands that short-circuiting threw away. Nothing here is a compiler bug, and no warning is issued: the program did precisely what the language says it should. It just is not what the author meant.
Anything on the right of a `&&` or a `||` might never run. Keep increments, decrements, assignments, and calls that change state out of those positions, and put them in their own statement where they always execute.
The Reach of !
Logical NOT has very high precedence, higher than any comparison operator. That makes it grab a single operand tightly and stop there, which is not what a beginner expects when a comparison is nearby. The following program is wrong, and it is worth reading closely before the output:
#include <iostream>
int main()
{
int reading{42};
int alarmLevel{75};
if (!reading > alarmLevel)
std::cout << "Reading is below the alarm level\n";
else
std::cout << "Reading has reached the alarm level\n";
return 0;
}
s.cpp:8:18: warning: logical not is only applied to the left hand side of comparison [-Wlogical-not-parentheses]
8 | if (!reading > alarmLevel)
| ^
Reading has reached the alarm level
A reading of 42 has plainly not reached an alarm level of 75, so the message is backwards. Because ! binds more tightly than >, the condition groups as (!reading) > alarmLevel. The ! is applied to reading alone, converting 42 to true and then negating it to false, which becomes 0 in the comparison. The test the program actually performs is 0 > 75, which is false, so the else branch runs.
The compiler is helpful here, but only because this particular shape is a known trap. Parentheses fix it by giving ! the whole comparison as its operand:
#include <iostream>
int main()
{
int reading{42};
int alarmLevel{75};
if (!(reading > alarmLevel))
std::cout << "Reading is below the alarm level\n";
else
std::cout << "Reading has reached the alarm level\n";
return 0;
}
Reading is below the alarm level
Now reading > alarmLevel is computed first, and ! flips the bool that comes out.
Give `!` an explicitly parenthesised operand whenever it should negate the result of some other operator, as in `!(reading > alarmLevel)`. Negating a single name, as in `!hatchOpen`, involves no other operator and needs no parentheses.
Testing One Variable Against Several Values
Chaining is where logical operators earn their keep. Both binary operators associate left to right, so you can string as many as you need together:
if (sensorId == 4 || sensorId == 6 || sensorId == 9)
std::cout << "Recognised sensor\n";
A chain of && works the same way, and is the usual way to pin a number inside a range while excluding a value:
if (windSpeed > 15 && windSpeed < 40 && windSpeed != 27)
std::cout << "Wind speed is in range and not the calibration value\n";
Notice that each of those operands names sensorId or windSpeed again. That is not redundancy: each operand of || and && has to be a complete condition in its own right. This is where the most common beginner mistake with logical operators appears. The next program is broken, and unlike the earlier trap the compiler says nothing at all:
#include <iostream>
int main()
{
int sensorId{9};
if (sensorId == 4 || 6)
std::cout << "Sensor 4 or 6 reported\n";
else
std::cout << "A different sensor reported\n";
return 0;
}
Sensor 4 or 6 reported
The sensor is number 9, yet the program claims it was 4 or 6. English reads sensorId == 4 || 6 as "sensorId equals 4 or 6", but C++ reads it as (sensorId == 4) || 6. The right operand is the literal 6 on its own, which converts to bool as true because it is non-zero. So the whole condition is something || true, which is true for every possible value of sensorId.
A condition like `sensorId == 4 || 6` compiles cleanly and is always `true`. Name the variable again in every operand: `sensorId == 4 || sensorId == 6`.
Two other symbols are easy to confuse with these. & and | are the bitwise AND and OR operators, covered later with bit manipulation. They work on the individual bits of their operands, they do not short-circuit, and a single missing character turns a correct condition into one that quietly computes something else.
&& Binds Tighter Than ||
Because && and || look like a matched pair, it is natural to assume they sit at the same precedence level and simply group left to right, the way + and - do. They do not. Logical AND sits one precedence level above logical OR, so every && in an expression is grouped with its operands before any || gets a turn.
An expression mixing the two without parentheses is a warning on this compiler. The program below is wrong for exactly that reason:
#include <iostream>
int main()
{
bool ventJammed{true};
bool frostRisk{true};
bool heaterOn{false};
std::cout << std::boolalpha;
std::cout << (ventJammed || frostRisk && heaterOn) << '\n';
return 0;
}
s.cpp:10:43: warning: suggest parentheses around '&&' within '||' [-Wparentheses]
10 | std::cout << (ventJammed || frostRisk && heaterOn) << '\n';
| ~~~~~~~~~~^~~~~~~~~~~
true
The two possible groupings are not interchangeable, and with these three values they disagree:
#include <iostream>
int main()
{
bool ventJammed{true};
bool frostRisk{true};
bool heaterOn{false};
std::cout << std::boolalpha;
std::cout << "grouped as C++ groups it: " << (ventJammed || (frostRisk && heaterOn)) << '\n';
std::cout << "grouped left to right: " << ((ventJammed || frostRisk) && heaterOn) << '\n';
return 0;
}
grouped as C++ groups it: true
grouped left to right: false
C++ takes the first reading: ventJammed || (frostRisk && heaterOn). A programmer who assumed left-to-right grouping would have been expecting the second, and would have got the opposite answer with no indication that anything went wrong beyond the warning.
Any expression containing both `&&` and `||` should carry parentheses that spell the grouping out, so that neither you nor a later reader has to recall which one binds first. Write `(batteryLow && alarmArmed) || (frostRisk && heaterOn)` rather than leaving the shape to precedence.
Pushing ! Through a Condition
Negating a compound condition trips up a lot of people, because ! does not distribute the way a minus sign does over addition. !(frosty && windy) is emphatically not !frosty && !windy.
De Morgan's laws give the correct rewrites, and they come in a matched pair:
!(a && b)is equivalent to!a || !b!(a || b)is equivalent to!a && !b
The pattern is the same in both directions. Push the ! onto each operand, and flip the operator in the middle: && becomes ||, and || becomes &&. Forgetting the flip is what produces the wrong version above.
Both laws are small enough to check exhaustively. Two bool variables have four possible combinations, so a program that prints all four rows is a complete proof:
#include <iostream>
void showRow(bool frosty, bool windy)
{
std::cout << frosty << '\t' << windy << '\t'
<< !(frosty && windy) << '\t' << (!frosty || !windy) << '\t'
<< !(frosty || windy) << '\t' << (!frosty && !windy) << '\n';
}
int main()
{
std::cout << std::boolalpha;
std::cout << "frosty\twindy\t!(f&&w)\t!f||!w\t!(f||w)\t!f&&!w\n";
showRow(false, false);
showRow(false, true);
showRow(true, false);
showRow(true, true);
return 0;
}
frosty windy !(f&&w) !f||!w !(f||w) !f&&!w
false false true true true true
false true true true false false
true false true true false false
true true false false false false
Column three matches column four in every row, and column five matches column six in every row. There are no other rows to check, so the two laws hold for all bool values.
These rewrites are worth knowing because the negated form is often the readable one. !(frosty && windy) and !frosty || !windy compute the same thing, and you get to pick whichever states your intent more directly.
C++ Has No Logical XOR
Some languages offer a logical exclusive-or, an operator that is true when its operands differ. C++ does not. operator^ exists, but it is bitwise XOR, not a logical operator, and it cannot short-circuit the way && and || do.
There is a direct substitute for bool operands. "The two differ" is exactly what != tests:
#include <iostream>
int main()
{
bool dayLogged{true};
bool nightLogged{true};
std::cout << std::boolalpha;
std::cout << "exactly one shift filed a log: " << (dayLogged != nightLogged) << '\n';
return 0;
}
exactly one shift filed a log: false
Both shifts filed, so the operands match and != reports false, which is the answer exclusive-or would give. The idea extends to more operands: (a != b) != c is true when an odd number of the three is true. Write those parentheses in, because leaving them out earns a -Wparentheses warning from this compiler even though the grouping is the same.
The `!=` substitute only works when every operand is already of type `bool`. Applied to `int` values it compares the numbers themselves, so `4 != 6` is `true` even though both operands are non-zero and an exclusive-or would report `false`.
The Word Spellings
For historical reasons, some keyboards and character sets could not produce every punctuation mark C++ needs. The language therefore defines spelled-out keywords for several operators, including all three logical ones.
| Symbol | Keyword |
|---|---|
&& |
and |
|| |
or |
! |
not |
They are not macros or a library feature. They are part of the language, and the two spellings compile to exactly the same thing:
#include <iostream>
int main()
{
bool hatchOpen{false};
bool alarmArmed{true};
if (not hatchOpen and alarmArmed)
std::cout << "Word spelling: the greenhouse is secure\n";
if (!hatchOpen && alarmArmed)
std::cout << "Symbol spelling: the greenhouse is secure\n";
return 0;
}
Word spelling: the greenhouse is secure
Symbol spelling: the greenhouse is secure
The word spellings may look friendlier at this stage, but the C++ code you will read in the wild is written almost entirely with the symbols. Learn to read &&, ||, and ! fluently, and use them in your own code.
Key Terminology
- Logical operator: An operator that converts its operands to
booland produces abool, namely!,&&, and|| - Logical NOT (
!): A unary operator producing the oppositeboolof its operand - Logical AND (
&&): A binary operator producingtrueonly when both operands aretrue - Logical OR (
||): A binary operator producingtruewhen at least one operand istrue - Short-circuit evaluation: Skipping the right operand entirely once the left operand has settled the result
- Side effect: An observable change an expression makes beyond producing a value, such as the increment in
++retryCount - De Morgan's laws: The pair of rewrites
!(a && b)to!a || !b, and!(a || b)to!a && !b - Bitwise operators (
&,|,^): Operators that work on individual bits rather than truth values, and never short-circuit
Looking Forward
Every condition here has been a single expression evaluated in one shot. The next chapter turns to program flow, where these conditions become the steering mechanism for if, else if, switch, and loops. Once a condition controls whether a loop body repeats, short-circuiting stops being a curiosity and becomes something you write around deliberately, putting the cheap or protective test on the left every time.
Summary
- The three logical operators are
!(NOT),&&(AND), and||(OR); they convert their operands tobooland produce abool !produces the opposite of its single operand:!trueisfalse, and!falseistrue&&producestrueonly when both operands aretrue;||producestruewhen at least one operand istrue- Unlike most operators,
&&and||guarantee that the left operand is evaluated first - Short-circuit evaluation means the right operand is skipped entirely once the answer is settled: a
falseon the left of&&, or atrueon the left of|| - Short-circuiting is a correctness tool, not just a speed trick; put the precondition on the left, as in
sampleCount != 0 && rainfallTotal / sampleCount > 12 - A side effect on the right of
&&or||may never happen, which is whystationId == 3 && ++retryCount == 1can leaveretryCountuntouched !has higher precedence than any comparison, so!reading > alarmLevelmeans(!reading) > alarmLevel; write!(reading > alarmLevel)instead- Every operand must be a complete condition:
sensorId == 4 || 6is alwaystrue, because the literal6converts totrueon its own &&has higher precedence than||, soa || b && cgroups asa || (b && c); parenthesise every mixed expression explicitly- De Morgan's laws distribute
!across a compound condition and flip the operator:!(a && b)is!a || !b, and!(a || b)is!a && !b - C++ has no logical XOR, but
!=gives the same result forbooloperands and,or, andnotare language-defined alternative spellings of&&,||, and!; the symbolic forms dominate real code
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Boolean Logic with Logical Operators - Quiz
Test your understanding of the lesson.
Practice Exercises
Logical Operators
Practice using logical operators for combining conditions.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!