Operators and expressions
An expression is a value built from operators and operands, and C++ has about forty operators that combine into a compact syntax. Most of them are familiar from arithmetic; the ones that cause bugs are integer division, the difference between = and ==, the short-circuit of && and ||, and the precedence of the bitwise operators.
This lesson runs each group on real values, so the results are visible instead of memorised, and it ends with the one precedence mistake that the compiler warns about and programs still ship.
Arithmetic and integer division
The five arithmetic operators work on integers and floating point values, and division means two different things. Between two ints it drops the fraction and keeps the quotient, and the remainder operator % recovers what was dropped.
Mixing an int with a double converts the int and gives a floating point result, which is why 17 / 5.0 is 3.4 while 17 / 5 is 3. The conversion happens before the division, not after.
% is defined only for integers. For negative operands the sign of the result follows the dividend, so -17 % 5 is -2, and a program that expects a positive remainder must handle that case.
#include <iostream>
int main()
{
int a = 17;
int b = 5;
std::cout << a + b << ' ' << a - b << ' ' << a * b << '\n';
std::cout << "integer division: " << a / b << '\n';
std::cout << "remainder: " << a % b << '\n';
std::cout << "floating division: " << a / 5.0 << '\n';
}
g++ -std=c++23 -Wall -Wextra arithmetic.cpp -o arithmetic
./arithmetic
22 12 85
integer division: 3
remainder: 2
floating division: 3.4
Comparisons produce bool
Every comparison yields a bool, so a comparison can be stored, returned and printed. std::boolalpha makes the stream print true and false instead of 1 and 0; without it the digits are the values of the bool.
The operators are ==, !=, <, <=, > and >=. C++ has no chained comparison: a < b < c compiles, compares the bool with c, and is almost never what the author meant. GCC warns about the chain, as the output below shows, and the fix is a < b && b < c.
The classic typo is = where == belongs. An assignment inside an if is legal, the assigned value becomes the condition, and the program compiles; -Wparentheses reports the ones the compiler can see. Write the condition so that it cannot be read as an assignment, or compare against an explicit value.
#include <iostream>
int main()
{
int a = 3;
int b = 3;
int c = 5;
std::cout << std::boolalpha;
std::cout << "a == b: " << (a == b) << '\n';
std::cout << "a != b: " << (a != b) << '\n';
std::cout << "a < c: " << (a < c) << '\n';
std::cout << "a < b < c: " << (a < b < c) << '\n';
}
g++ -std=c++23 -Wall -Wextra comparison.cpp -o comparison
./comparison
comparison.cpp: In function 'int main()':
comparison.cpp:13:38: warning: comparisons like 'X<=Y<=Z' do not have their mathematical meaning [-Wparentheses]
13 | std::cout << "a < b < c: " << (a < b < c) << '\n';
| ~~^~~
a == b: true
a != b: false
a < c: true
a < b < c: true
Short-circuit evaluation
&& stops at the first false operand and || stops at the first true one, because the result is already decided. That is not an optimisation detail: code relies on it to guard a null pointer or an empty container before reading through it.
The program prints a line whenever a function is called, so the output shows which operands were evaluated. In the first expression the left side is false, the right side never runs, and the printed result is 0. In the second the left side is true and the right side is skipped again.
The calls print in the middle of the line, before the result, because they run while the expression is evaluated. That is what the order of the output shows: a called, then the completed first line, then c called, then the second line.
The order matters for correctness and for side effects. A function call that must happen belongs on the left or outside the expression, never on the right of a short circuit that may skip it.
#include <iostream>
bool yes(const char* name)
{
std::cout << name << " called\n";
return true;
}
bool no(const char* name)
{
std::cout << name << " called\n";
return false;
}
int main()
{
std::cout << "first: " << (no("a") && yes("b")) << '\n';
std::cout << "second: " << (yes("c") || no("d")) << '\n';
}
g++ -std=c++23 -Wall -Wextra logical.cpp -o logical
./logical
first: a called
0
second: c called
1
Bitwise operators as flags
The bitwise operators work on the bits of an integer: & keeps the bits set in both operands, | joins them, ^ flips the ones set in one operand, ~ inverts all of them, and << and >> shift left and right. On an unsigned type every one of them is well defined.
One bit per flag is the oldest way to store a set: read is 1, write is 2 and exec is 4, and a variable holds any combination. Setting a flag is |=, testing it is (flags & read) != 0, and clearing it is &= ~read.
Use unsigned for bit patterns. Shifting a signed value into its sign bit is undefined, and a mask built from a signed 1 << 31 is a bug on most machines.
#include <iostream>
int main()
{
const unsigned int read = 1u << 0;
const unsigned int write = 1u << 1;
const unsigned int exec = 1u << 2;
unsigned int flags = 0;
flags |= read;
flags |= exec;
std::cout << "flags: " << flags << '\n';
std::cout << "read set: " << ((flags & read) != 0) << '\n';
std::cout << "write set: " << ((flags & write) != 0) << '\n';
std::cout << "without exec: " << (flags & ~exec) << '\n';
std::cout << "1 << 10: " << (1u << 10) << '\n';
}
g++ -std=c++23 -Wall -Wextra bitwise.cpp -o bitwise
./bitwise
flags: 5
read set: 1
write set: 0
without exec: 1
1 << 10: 1024
Precedence, and the warning it earns
Every operator has a precedence, and == binds tighter than &. The condition value & 1 == 0 is therefore value & (1 == 0), which is value & 0, which is always false: the program calls 4 odd.
The compiler sees the pattern and warns that parentheses would make the intent clear. That warning is the cheapest fix in this lesson: add them, and the condition becomes (value & 1) == 0, which is true for even numbers.
The full table is worth a look once: postfix operators bind tighter than unary, unary tighter than multiplication, then addition, shifts, comparisons, bitwise and, xor, or, logical and, logical or, and assignment last. When in doubt, add parentheses; they cost nothing at run time.
#include <iostream>
int main()
{
int value = 4;
if (value & 1 == 0) {
std::cout << value << " is even\n";
} else {
std::cout << value << " is odd\n";
}
}
g++ -std=c++23 -Wall -Wextra precedence.cpp -o precedence
./precedence
precedence.cpp: In function 'int main()':
precedence.cpp:6:19: warning: suggest parentheses around comparison in operand of '&' [-Wparentheses]
6 | if (value & 1 == 0) {
| ~~^~~~
4 is odd
Exercises
- Print the result of -17 / 5 and -17 % 5. Do they have the sign you expected?
- Rewrite the logical program so that both functions are always called, using a temporary bool instead of a short circuit.
- Build a mask with three flags, set two of them, clear one and print the result in decimal. Which values can the mask take?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does integer division do with a remainder?
Answer
It truncates toward zero and the remainder is separate
The / operator on two ints gives the quotient, and the % operator gives what is left.
-
What does
a < b < cmean in C++?Answer
(a < b) < c, comparing a bool with cRelational operators are left-associative, and the compiler warns about the chain.
-
What does short-circuit evaluation of
&&mean?Answer
The right operand is not evaluated when the left one is false
The result is known from the left operand, so the right side is skipped, which is why a null check can guard a dereference.
-
What is the difference between
=and==?Answer
=assigns a value,==compares two valuesAn assignment inside a condition is legal and is a common source of bugs, which is why warnings mention it.
-
What do the arithmetic results print?
Answer
22 12 85, integer division: 3, remainder: 2, floating division: 3.4
The program prints the three arithmetic results and the two divisions, one integer and one floating.
-
What does this program print?
#include <iostream> int main() { int a = 1, b = 2, c = 3; std::cout << std::boolalpha << (a < b < c); }Answer
true
It compares (a < b), which is true, with c, so the chain evaluates to true; the compiler warns about the chain.
-
What does this program print?
#include <iostream> bool a() { std::cout << "a called "; return false; } bool b() { std::cout << "b called "; return true; } bool c() { std::cout << "c called "; return true; } bool d() { std::cout << "d called "; return false; } int main() { std::cout << "first: "; bool one = a() && b(); std::cout << one << ", second: "; bool two = c() || d(); std::cout << two; }Answer
first: a called 0, second: c called 1
&& stops after a() returns false, and || stops after c() returns true, so the two inner calls are skipped.
-
What does this program print?
#include <iostream> int main() { std::cout << (1 << 10) << '\n'; }Answer
1024
One shifted left ten places is 2 to the tenth, 1024.
-
What does the compiler warn about in
if (value & 1 == 0)?Answer
suggest parentheses around comparison in operand of '&'
The comparison binds tighter than the bitwise and, so the expression means
value & (1 == 0). -
What does this program print?
#include <iostream> int main() { int value = 4; if (value & 1 == 0) std::cout << value << " is even"; else std::cout << value << " is odd"; }Answer
4 is odd
The comparison binds first, so the condition is value & (1 == 0), which is 0 and false, and the else branch prints.
The code on this page was run with g++ 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev