Operators: arithmetic, comparison, logic
The previous lesson included a warning: 10 / 3 in int gives not 3.33, but simply 3. It's time to understand why — and also to cover comparison and logical operators, without which you can't build a single condition in the next lesson.
01What you'll learn
- Arithmetic operators
+ - * / %and the integer division trap - Comparison operators
== != < > <= >=— their result is alwaysbool - Logical operators
&& || !and what short-circuit evaluation is - A common mistake — confusing
=(assignment) with==(comparison)
02Core idea: operators are still a calculator, just with new buttons
A regular calculator knows + - * /. C++ adds to this set: % (division remainder), comparison buttons (equal/greater/less — but instead of a number, they produce "yes" or "no"), and logic buttons that combine several "yes/no" answers into one. There's no new concept here — an operator is still part of an expression that evaluates to a value, just like x + 5 from the second lesson. It's just that result types now vary: arithmetic produces a number, comparison and logic always produce bool.
03Arithmetic operators
Addition and subtraction already came up (x + 5) — the remaining three from the full set (* / %) work on the same principle: each operator is part of an expression that evaluates to a value, not a command by itself.
It's exactly this set where the trap warned about in the previous lesson occurred. On paper, 17 divided by 5 is 3.4. In C++, if both variables are int, 17 / 5 is exactly 3, with no rounding — the fractional part simply doesn't appear. The computer isn't making a mistake or glitching here: if the result must be int, and int cannot store fractions, the only option is to discard the fractional part entirely. To get an exact result, at least one operand must be double before the division:
int total = 17;
int players = 5;
int perPlayerInt = total / players; // 3 — fractional part discarded
int remainder = total % players; // 2 — remainder, what didn't fit
double perPlayerExact = static_cast<double>(total) / players; // 3.4 — exact result
% (division remainder) is what complements integer division: total / players gives "how much each got", total % players gives "how much remained undistributed". static_cast<double>(total) is an explicit type cast: it converts the value of total from int into a temporary value of type double right before the division, so the division follows double rules rather than int rules (more on type casts in Stage 01). The construct has three parts, each with its own role:
static_cast— the operator itself: the command to "convert explicitly", not "add" or "compare".<double>— the target type in angle brackets: what exactly to convert the value into.(total)— the source expression in parentheses: what exactly is being converted; the result is a new temporary value, whiletotalitself remainsintand is unchanged.
The result of the whole construct is an ordinary value of type double, which you can then work with like any other double (for example, immediately divide by players). static_cast is not the only way to convert one value into another; it has three neighboring alternatives, and the choice between them is not random:
| Variant | When |
|---|---|
static_cast<T>(x) | ordinary controlled conversion — the standard choice (as here, int → double) |
| implicit conversion | when the compiler safely performs the needed conversion itself — for example, double d = 5; with no static_cast at all |
C-style (T)x | inherited from the C language; generally avoid in modern C++ — doesn't express intent and is more dangerous than static_cast |
reinterpret_cast | a completely different, much rarer and more dangerous task — not for numbers; out of scope for this course |
04Comparison operators
"Is the character alive?" — essentially the question "is hp greater than zero?", and the answer to such a question is not a number but yes/no. That's exactly what comparison operators do: they take two values and turn them into a bool — true or false, nothing else:
| Operator | Means | Example |
|---|---|---|
== | equal to | hp == 0 |
!= | not equal to | hp != 0 |
< | less than | hp < 20 |
> | greater than | hp > 0 |
<= | less than or equal to | hp <= 0 |
>= | greater than or equal to | hp >= 100 |
The most common beginner mistake is writing hp = 0 where the comparison hp == 0 was intended. This is not a typo the compiler will catch: hp = 0 is a perfectly legal assignment (see the second lesson), it just doesn't compare — it changes the value of hp to 0. More in the "Common mistakes" section.
05Logical operators
A single comparison is often not enough: "can continue" depends not only on whether the character is alive, but also on whether they have a key — two separate bool values need to be combined into one answer. That's what logical operators are for: they take one or more bool values and combine them into a new bool value:
| Operator | Means | Example | True when |
|---|---|---|---|
&& | AND (and) | hp > 0 && hasKey | both conditions are true |
|| | OR (or) | hp > 0 || hasShield | at least one condition is true |
! | NOT (not) | !isAlive | flips the value: was true — became false |
&& and || evaluate with short-circuit evaluation: in a && b, if a is already false, C++ doesn't even evaluate b — the result will be false in any case. Similarly for a || b: if a is already true, b is not evaluated. This isn't critical now, but it will become important later, when b is not just a variable but a function call with a side effect — short-circuit evaluation means such a call may not happen at all.
06Operator precedence
Just like in mathematics (where multiplication happens before addition), C++ operators have an evaluation order. From highest to lowest precedence:
| Group | Operators | Example |
|---|---|---|
| 1. Parentheses | () | (a + b) * c — parentheses first |
| 2. Unary NOT | ! | !isAlive |
| 3. Multiplication, division, remainder | * / % | a + b * c — b * c first |
| 4. Addition, subtraction | + - | hp - damage > 0 — subtraction first |
| 5. Comparison | < > <= >= | hp > 0 && hasKey — comparison first |
| 6. Equality | == != | hp == 0 || hasShield — equality first |
| 7. Logical AND | && | a && b || c — a && b first |
| 8. Logical OR | || | evaluated after && |
| 9. Assignment | = | result = a && b — evaluated last |
You don't have to keep the whole table in your head: when in the slightest doubt, add parentheses explicitly — (hp - damage) > 0 reads instantly, without needing to recall precedence.
07Building the "can proceed" condition step by step
The character program from the previous two lessons grows further: hp gains damage (damage from a hit) and hasKey (whether there is a key) — new variables for new mechanics, not directly related to the types from the previous lesson, but the same program. Same principle as before: one program that gains one more operator kind at each step — first arithmetic, then comparison, then logic.
int hp = 40;
int damage = 25;
int hpAfterHit = hp - damage;
std::cout << "HP after hit: " << hpAfterHit << "\n";
int hp = 40;
int damage = 25;
int hpAfterHit = hp - damage;
bool isAlive = hpAfterHit > 0;
std::cout << "HP after hit: " << hpAfterHit << ", Alive: " << isAlive << "\n";
int hp = 40;
int damage = 25;
bool hasKey = true;
int hpAfterHit = hp - damage;
bool isAlive = hpAfterHit > 0;
bool canProceed = isAlive && hasKey;
std::cout << "Alive: " << isAlive << ", Can proceed: " << canProceed << "\n";
int hp = 40;
int damage = 25;
bool hasKey = true;
bool canProceed = (hp - damage) > 0 && hasKey; // - before >, > before &&
std::cout << "Can proceed: " << canProceed << "\n";
The parentheses around (hp - damage) are not strictly required here — per the table above, subtraction would happen before comparison anyway — but they make the expression immediately readable, without cross-checking the precedence table.
08Worse → better: = instead of == — a silent bug
The task is to check that hp equals 0, and also account for having a key. One character here changes behavior completely, and the compiler won't warn about it.
int hp = 0;
bool isAlive = (hp = 5); // typo: assignment, not comparison
std::cout << isAlive << " " << hp;
Compiles without a single warning on many configurations — hp = 5 is syntactically a perfectly legal expression (an assignment statement from the second lesson), it just ended up in the wrong place here.
The program didn't crash or complain — it simply did something other than what was intended: hp was overwritten with 5, and isAlive became true, because the result of an assignment (the value 5 itself) when converted to bool — any nonzero number becomes true. This is more dangerous than a compilation error: there is nothing to fix "per the compiler's error", the bug has to be spotted by you.
int hp = 0;
bool isAlive = (hp == 5); // comparison
std::cout << isAlive << " " << hp;
Now hp stays equal to 0 (comparison doesn't change the value), and isAlive is honestly false, because 0 ≠ 5.
int hp = 0;
bool hasKey = true;
bool canProceed = (hp == 0) && hasKey;
std::cout << "hp: " << hp << ", canProceed: " << canProceed << "\n";
The parentheses around hp == 0 are not strictly required here (by precedence, the comparison would happen before && anyway), but they explicitly show exactly what is being compared before the result participates in the logical AND — that's precisely the habit that saves debugging time on similar typos in the future.
09Try changing it
- In the example from the "Arithmetic operators" section, replace
totalwith 20 —perPlayerIntbecomes 4,remainderbecomes 0,perPlayerExactstays 4.0. - Output
hp > 0directly viastd::cout— you'll see1or0, not the words true/false (same as with a plainboolin the previous lesson). - Replace
&&with||in the two-variable condition — the result may change if at least one part is true.
10Live debugging: common mistakes
An important difference from previous lessons: all three mistakes below are not compilation errors. The code will build and run successfully in every case; the problem only appears at runtime — either as an incorrect result or as a program crash. These are exactly the bugs that are hardest to find, because the compiler gives no hint where to look.
= instead of ==if (hp = 0) // typo: assignment instead of comparison (should be ==)
A condition with this expression will always be false, and hp will be silently zeroed. Full breakdown with sample output is in the "Worse → better" section above; the if syntax is covered in detail in the next lesson.
int total = 17;
int players = 5;
double average = total / players; // divides as int/int BEFORE assignment to double
std::cout << average;
The fractional part is lost at the moment of the division itself, not when storing into double — the variable average merely stores the already-rounded result 3 as 3.0. You need to cast to double before division: static_cast<double>(total) / players.
int total = 17;
int divisor = 0;
int x = total % divisor; // division/remainder by 0 for int — undefined behavior
In practice this is most often an immediate program crash (the message above is a typical appearance on Linux, despite the name "floating point" — a historical naming detail of that signal; the division itself here is integer). Before dividing by a variable that could theoretically be 0, it's worth ensuring it isn't — this check will become natural once if appears in the next lesson.
11Check yourself
Three questions: predicting the result of integer division, spotting a classic typo, and breaking down operator precedence.
a equal to — and why not 3.4?int total = 17;
int players = 5;
int a = total / players;Show explanation
a = 3 — integer division int / int discards the fractional part entirely, without rounding to the nearest integer. To get 3.4, you need to cast at least one operand to double BEFORE the division (static_cast<double>(total) / players).
hp is zero. The compiler accepted the code without a single warning. What will the program actually output?int hp = 10;
bool isAlive = (hp = 0);
std::cout << hp << " " << isAlive;Show explanation
It outputs 0 0. hp = 0 is not a comparison but an assignment (a single = instead of ==): hp becomes 0, and the result of the entire assignment expression is the assigned value itself (0), which when converted to bool gives false — zero is the only number that gives false. The compiler is not obligated to flag this; it's legal syntax (see §08).
bool result = hp - damage > 0 && hasKey;Show explanation
First hp - damage (arithmetic, group 4 in the table in §06), then the result is compared with 0 (comparison, group 5), and only then is that comparison combined with hasKey via && (group 7) — arithmetic and comparison always happen before logical operators. Explicit parentheses — (hp - damage) > 0 && hasKey — would produce the same result, but read faster, without checking the table.
12Practice
★★ Based on total and players, compute: integer division (/), remainder (%), and exact division in double (with a type cast). Output all three values.
Three separate variables — integer division, remainder, and division cast to double (§03) — and all three must actually reach std::cout, not just be computed.
Solution: int perPlayerInt = total / players; int remainder = total % players; double perPlayerExact = static_cast<double>(total) / players; — then output all three via std::cout.
★★ You have hp and hasKey. Compute and output via std::cout the result of a comparison (hp > 0 or hp == 0) and the result of a logical AND/OR combining that comparison with hasKey.
First compare hp with 0 — the result is bool, save it to a variable. Then combine that variable with hasKey via && or || — also into a variable. Both variables must reach std::cout.
Solution: bool isAlive = hp > 0; bool canProceed = isAlive && hasKey; — then std::cout << isAlive << canProceed;
★★★ Given int hp, int damage, and bool hasShield. Compute hp after damage (arithmetic), check that it's greater than 0 (comparison), and compute canProceed — you can proceed if the character is alive OR has a shield (logic). Use at least one operator from each group: arithmetic, comparison, logic. Output the result via std::cout.
Three steps in a row, as in the "Building the condition" section above: first arithmetic (hp - damage), then comparison of the result with 0, then logic — the comparison is combined with hasShield.
Each of the three results — arithmetic, comparison, logic — must actually reach std::cout, not just be computed somewhere in the body of main().
Solution: int hpAfterHit = hp - damage; bool isAlive = hpAfterHit > 0; bool canProceed = isAlive || hasShield; — then std::cout << hpAfterHit << isAlive << canProceed; (you can also do it in a single expression, as in step 4 of the "Building the condition" section above).
13Terms
| Term | Means |
|---|---|
| Operator | a symbol like +, ==, && that computes a new value from one or more operands |
| Integer division | division of int by int, where the fractional part of the result is discarded |
| Operator precedence | the order in which operators are evaluated in a single expression without parentheses |
| Short-circuit evaluation | skipping evaluation of the second operand of &&/|| when the result is already clear from the first |
14What you can do now
Before this lesson, variables could only be stored and output. Now you can compute with them:
- You can predict the result of integer division
int / intand explain why, for an exact result, you need to cast one operand todoublebefore the division, not after. - You can turn any condition of the form "A is greater/equal/less than B" into an expression of type
boolusing comparison operators. - You can combine several
boolconditions into one via&&/||/!and predict when short-circuit evaluation will kick in. - You can spot a typo of
=instead of==in someone else's or your own code and explain why the compiler doesn't catch it. - You can apply the operator precedence table to predict the evaluation order of a mixed expression of arithmetic, comparison, and logic — or add parentheses if you're unsure.
15Summary
+ - * / %— arithmetic;/for twoints discards the fractional part; for an exact result, cast todoublebefore division.%— remainder of integer division.== != < > <= >=— comparison, the result is alwaysbool.&& || !— logical operators onbool;&&/||evaluate with short-circuit evaluation.=(assignment) and==(comparison) are different operators; confusing them is not always caught by the compiler.
Now you can compute a bool condition like canProceed — but you still can't do anything with it depending on the result. The next lesson introduces if/else: the very thing this whole lesson about comparison and logic was building toward.
All these operators are part of the C++ language itself and work in Unreal code unchanged. For more complex math (powers, roots, min/max, range clamping), Unreal adds the FMath library on top of the standard operators:
float clampedHp = FMath::Clamp(hpAfterHit, 0.0f, 100.0f); // keeps HP from going outside 0..100
float dist = FMath::Sqrt(FMath::Square(dx) + FMath::Square(dy)); // distance between points
And for vector quantities (position, velocity, direction — triples {X, Y, Z}), the same arithmetic and logical operators work through the FVector type, which overloads them:
FVector velocity = FVector(1.0f, 0.0f, 0.0f) * speed; // the same *, but for a vector
bool reachedGoal = FVector::Dist(currentPos, goalPos) < 50.0f; // the same <
The basic + - * / % == && || and the precedence between them remain exactly the same as in this lesson — Unreal just adds types and functions on top of them. Details on FMath and FVector are in Stage 07+.