C++ / OPERATORS AND EXPRESSIONS
Bitwise operators and masks
Use &, |, ^, ~, << and >> with masks to set, clear, toggle, test and extract individual bits inside an integer safely.
What you will learn
- Set with x |= M, clear with x &= ~M, toggle with x ^= M, test with (x & M) != 0
- Extract a field with (x >> pos) & mask: the shift picks position, the mask picks width
- Use unsigned types for bit work; >> on a negative int copies the sign bit
- Parenthesise mask tests, because & binds looser than == and !=
Understanding Bitwise operators and masks
An integer is a fixed-width row of bits, and &, | and ^ work on each position independently: bit 3 of the result depends only on bit 3 of the two operands. What makes masks work is that each of these operators has a bit value that leaves the other operand alone: b & 1 is b, b | 0 is b, b ^ 0 is b. So a mask is really a per-position instruction — a 1 in an & mask means keep this bit while a 0 forces it to zero, a 1 in an | mask forces the bit to one, and a 1 in a ^ mask flips it. That is why clearing needs a complement: x &= ~M puts 0 exactly where M had 1s and 1s everywhere else.
The shift operators place bits rather than combine them. 1u << n is how you name bit n, and OR-ing several of those together builds a multi-bit mask. Shifting left drops the bits that fall off the top and fills zeros at the bottom, so nothing wraps around; a shift count equal to or larger than the width of the promoted left operand is undefined behaviour, not a silent zero. Reading a packed field is both moves combined: shift right until the field's low bit sits at position 0, then AND with a mask as wide as the field to discard whatever was sitting above it.
Types matter more here than in ordinary arithmetic. Anything narrower than int is promoted first, so with std::uint8_t M = 2, ~M is the int -3; x &= ~M still works because the extra high 1s are truncated on the way back into x, but printing or comparing ~M will surprise you. Signed types also make the top bit special: >> on a negative int copies the sign bit inward, which C++20 defines as flooring division by a power of two and earlier standards left implementation-defined, so bit twiddling belongs on unsigned types. And these are bit operations, not boolean ones — flags & Write evaluates to the masked bits themselves, which is why 2 & 1 is 0 even though both operands are nonzero.
placeholder
<bitset>
<iostream>
constexpr unsigned Read = 1u << 0; // 0000 0001
constexpr unsigned Write = 1u << 1; // 0000 0010
constexpr unsigned Execute = 1u << 2; // 0000 0100
void show(const char* label, unsigned bits) {
std::cout << label << std::bitset<8>(bits) << '\n';
}
int main() {
unsigned perms = 0;
show("start : ", perms);
perms |= Read | Write; // a 1 in the mask forces a bit on
show("set R|W : ", perms);
perms &= ~Write; // a 0 in the mask forces a bit off
show("clear W : ", perms);
perms ^= Execute; // a 1 in the mask flips a bit
show("toggle X : ", perms);
std::cout << "has R : " << ((perms & Read) != 0) << '\n';
std::cout << "has W : " << ((perms & Write) != 0) << '\n';
std::cout << "R and X both : " << ((perms & (Read | Execute)) == (Read | Execute)) << '\n';
}
A mask is a per-bit instruction, and which mask you need follows from each operator's identity bit: & keeps where the mask is 1, | sets where it is 1, ^ flips where it is 1.
Worked examples
Unpacking a packed color
Pulls three byte-wide fields out of one integer with shifts and masks, then puts them back.
<iostream>
int main() {
unsigned color = 0x2C7FA3u; // packed as 0xRRGGBB
unsigned r = (color >> 16) & 0xFFu;
unsigned g = (color >> 8) & 0xFFu;
unsigned b = (color ) & 0xFFu;
std::cout << r << ' ' << g << ' ' << b << '\n';
unsigned rebuilt = (r << 16) | (g << 8) | b;
std::cout << std::hex << rebuilt << '\n';
std::cout << (rebuilt == color) << '\n';
}
Example explained
Line 1color >> 16 slides the red byte down until its low bit sits at position 0; green and blue fall off the right end and are gone.
Line 2& 0xFFu discards everything above those eight bits, which is what makes the green line correct: after >> 8 the red byte is still sitting above green.
Line 3For red the mask is redundant today because nothing is stored above bit 23, but keeping it means the line survives adding an alpha byte later.
Line 4Repacking is the mirror image: shift each component up to its slot and | them, which is safe only because the slots do not overlap.
The x & (x - 1) identity
Clears and isolates the lowest set bit, and uses the same trick to count set bits.
<bitset>
<iostream>
int main() {
unsigned x = 0b0101'1000u; // 88
std::cout << std::bitset<8>(x) << " x\n";
std::cout << std::bitset<8>(x - 1) << " x - 1\n";
std::cout << std::bitset<8>(x & (x - 1)) << " x & (x - 1): lowest set bit cleared\n";
std::cout << std::bitset<8>(x & ~(x - 1)) << " x & ~(x - 1): lowest set bit isolated\n";
int count = 0;
for (unsigned v = x; v != 0; v &= v - 1) ++count;
std::cout << count << " bits set in x\n";
}
Example explained
Line 1Subtracting 1 flips the lowest set bit of x to 0 and turns every 0 below it into 1, because that is how the borrow propagates.
Line 2So x & (x - 1) agrees with x above the lowest set bit and is 0 from that bit down, which removes exactly one bit per loop iteration and makes the loop run once per set bit.
Line 3~(x - 1) is 0 below the lowest set bit and the inverse of x above it, so AND-ing with x leaves only that one bit standing; x & -x does the same on unsigned types.
Line 4The same identity gives a power-of-two test: x != 0 && (x & (x - 1)) == 0.
Signed and unsigned right shift
Shows the same 32 bits shifting differently depending on whether the type is signed.
<iostream>
int main() {
int s = -8;
unsigned u = 0xFFFFFFF8u; // the same 32 bits, read as unsigned
std::cout << (s >> 1) << '\n';
std::cout << (u >> 1) << '\n';
std::cout << (s >> 2) << '\n';
std::cout << (-7 >> 1) << " vs " << (-7 / 2) << '\n';
}
Example explained
Line 1On a platform with a 32-bit int, s and u hold identical bit patterns, yet >> treats them differently.
Line 2s >> 1 copies the sign bit into the vacated top position, so -8 becomes -4 and the value stays negative.
Line 3u >> 1 shifts a 0 in instead, so the same bits read as 2147483644: for an unsigned type the top bit is just another data bit.
Line 4-7 >> 1 rounds toward negative infinity while -7 / 2 truncates toward zero, so >> is not a drop-in replacement for dividing signed values.
Important notes
With a multi-bit mask, decide between any and all: (x & M) != 0 means at least one bit of M is present, while (x & M) == M means all of them are.
C++20's <bit> header provides std::popcount, std::has_single_bit, std::countr_zero and std::rotl; they map to single machine instructions and accept only unsigned types, so prefer them to hand-written bit loops.
Common mistakes
Writing if (flags & Write == 0). It parses as flags & (Write == 0), that is flags & 0, so the condition is always false and the branch never runs.
Assuming ~M keeps the width of M. With std::uint8_t M = 0b0010, ~M is the int -3, so flags == ~M never matches and std::cout << ~M prints -3 instead of 253; write static_cast<std::uint8_t>(~M) when the value must stay eight bits wide.
Shifting by the width or more, like 1u << 32 on a 32-bit unsigned, or reaching for the sign bit with 1 << 31. A count at or above the width is undefined behaviour in every standard, and 1 << 31 overflowed a signed int before C++20; use 1u, 1ull, or a wider unsigned type.
Try it yourself
Change, predict, then run
Define Bold, Italic, Underline and Strike as 1u << 0 through 1u << 3, start from a value that has Bold and Underline set, then clear Underline, toggle Italic, and print std::bitset<4> of the value after each step. Finish by printing, with one mask comparison, whether Bold and Italic are both set.
Open the C++ workspaceCheck your understanding
Why does x &= ~M clear exactly the bits set in M while leaving every other bit of x unchanged?
- Because ~M holds 0 where M held 1 and 1 everywhere else, and b & 0 is always 0 while b & 1 is b
- Because & only touches the bit positions that are set in its right operand and ignores the others
- Because ~M is the two's-complement negation of M, so those bits get subtracted away
- Because compound assignment applies the operation position by position, which a plain & would not do
Show answer
& computes a result bit for every position, and the only bit value that passes the left operand through unchanged is 1, so you need 1s wherever x must survive and 0s where it must die — exactly what ~M gives you. Option 1 sounds right but is false, and its falseness is the whole reason the complement is needed: & writes a 0 wherever the right operand has a 0, including positions M never mentioned. Option 2 confuses ~M (one's complement) with -M, and no addition is involved.