Comparison, logical and bitwise operators are the three families that let a C++ program make decisions. Comparison operators produce a [[bool]] by asking a question, logical operators combine questions with [[&&]], [[||]] and [[!]], and bitwise operators work on the individual bits of an integer. The first two you will use every day; the third earns its place when you are packing several flags into one value.
Comparison operators answer questions
C++ has six comparison operators, all of which produce a bool — either true or false, which are numerically 1 and 0.
==equal to!=not equal to<less than>greater than<=less than or equal to>=greater than or equal to
#include
int main() {
int servings = 4;
int guests = 4;
std::cout << "equal: " << (servings == guests) << "\n";
std::cout << "not equal: " << (servings != guests) << "\n";
std::cout << "less than: " << (servings < guests) << "\n";
std::cout << "at least: " << (servings >= guests) << "\n";
std::cout << "in a range: " << (servings >= 2 && servings <= 8) << "\n";
return 0;
}A classic beginner mistake is writing a single equals sign where two are needed. = assigns; == compares. Because the mistake is so common, a compiler warning with -Wall will usually point it out.
Chained comparisons do not work the way they do in mathematics. 1 < servings < 5 parses as (1 < servings) < 5, which compares a boolean with an integer and is almost never what you meant. Write servings > 1 && servings < 5 instead.
Logical operators combine conditions
The three logical operators work on bool values.
&&logical and — true when both sides are true||logical or — true when either side is true!logical not — true when the operand is false
The important property is short-circuit evaluation. With &&, the right-hand side is evaluated only if the left-hand side is true. With ||, the right-hand side is evaluated only if the left-hand side is false.
#include
int calls = 0;
bool expensive_check() {
calls++;
return false;
}
int main() {
bool a = expensive_check() && expensive_check();
std::cout << "a=" << std::boolalpha << a << " calls=" << calls << "\n";
calls = 0;
bool b = expensive_check() || expensive_check();
std::cout << "b=" << b << " calls=" << calls << "\n";
return 0;
}The output shows the short-circuit at work:
a=false calls=1
b=false calls=2In the first case the left operand was false, so the right operand of && was never evaluated and only one call happened. In the second case the left operand was also false, so the right operand of || had to be evaluated — and it was, making two calls. Short-circuiting skips the right side only when the answer is already certain.
Short-circuiting is not an optimisation detail, it is a guarantee you can rely on. [[index < size && data[index] == target]] is safe because the second half is not evaluated when the index is out of range.
Bitwise operators work on bits
The bitwise operators treat their integer operands as collections of binary digits.
&bitwise and|bitwise or^bitwise exclusive or~bitwise not, flips every bit<<shift left, multiply by a power of two>>shift right, divide by a power of two
#include
int main() {
unsigned int a = 0b1100; // 12
unsigned int b = 0b1010; // 10
std::cout << "a = " << a << "\n";
std::cout << "b = " << b << "\n";
std::cout << "a & b = " << (a & b) << "\n";
std::cout << "a | b = " << (a | b) << "\n";
std::cout << "a ^ b = " << (a ^ b) << "\n";
std::cout << "~a = " << (~a) << "\n";
std::cout << "a << 2 = " << (a << 2) << "\n";
std::cout << "a >> 2 = " << (a >> 2) << "\n";
return 0;
}Each operator compares corresponding bits: AND keeps a bit only when both have it, OR keeps it when either has it, XOR keeps it when exactly one has it. The shifts move the bits along, which for unsigned values is exactly multiplication and division by two.
Bit flags combine several booleans
The classic use of bitwise operators is a set of flags packed into one integer. Each flag occupies its own bit, and the operators combine them without any of them interfering.
#include
enum Access {
Read = 1 << 0, // 0001
Write = 1 << 1, // 0010
Execute = 1 << 2, // 0100
Delete = 1 << 3 // 1000
};
int main() {
unsigned int perms = Read | Write;
perms |= Execute;
std::cout << "perms value: " << perms << "\n";
std::cout << "can read: " << ((perms & Read) != 0) << "\n";
std::cout << "can write: " << ((perms & Write) != 0) << "\n";
std::cout << "can delete: " << ((perms & Delete) != 0) << "\n";
perms &= ~Write; // turn the write flag off
std::cout << "after revoke, can write: " << ((perms & Write) != 0) << "\n";
return 0;
}The output shows the packed value and each individual test:
perms value: 7
can read: 1
can write: 1
can delete: 0
after revoke, can write: 0This is how file permissions, window styles and many operating system interfaces work. It is worth understanding even if you never write one, because you will meet it in every C library you use.
Choosing the right operator
- Use comparison operators to ask questions about values.
- Use
&&and||to combine questions, and rely on their short-circuit behaviour to guard expensive or unsafe operations. - Use bitwise operators when you are working with flags, masks, hardware registers or hash functions.
- Remember that the bitwise forms
&,|and^do not short-circuit, so never use them where&&or||was intended.
Quick reference
==compares; a single=assigns and is a common bug the compiler can warn about.- Comparisons yield
bool, which prints as 1 or 0 unless you setstd::boolalpha. - Chained comparisons do not work — use
&&instead. &&,||and!short-circuit, so the right side may never run.- The bitwise operators
&,|,^,~,<<and>>act on individual bits. - A mask is a bit pattern; AND tests bits, OR sets them, XOR toggles them.
- Shifts multiply and divide unsigned values by powers of two.
Try it yourself: write a small program that validates a recipe order: the number of servings must be between 1 and 12 and the scale factor must not be negative. Use [[&&]] so the second check only runs when the first passes, and add a bit-flag field marking the order as new, confirmed and cancelled. Then flip each flag and print the result.