6.6Bitwise operators (optional)
This lesson is optional
Bit manipulation is everyday work in graphics, embedded systems, compression, networking, and cryptography, and a curiosity everywhere else. Nothing in the upcoming chapters depends on this lesson; skip it freely and come back when a bitmask shows up in the wild.
Lesson 4.3 called binary literals plus underscores "the classic combination for bit-pattern work" and pointed here. This is the lesson where the bits themselves become the data.
A u8 is eight bits. Usually you treat the eight as one number; bitwise operators let you treat them as eight tiny switches. To see what we're doing, we need one new formatting verb from the brace mini-language (lesson 5.5's family): {:b} prints a number in binary, and {:08b} pads it to eight digits:
fn main() {
let x: u8 = 0b0000_0101;
println!("{x}");
println!("{x:08b}");
}5
00000101
Six operators work at this level:
| Operator | Name | Effect |
|---|---|---|
x & y | bitwise AND | 1 where both bits are 1 |
x | y | bitwise OR | 1 where either bit is 1 |
x ^ y | bitwise XOR | 1 where the bits differ |
!x | bitwise NOT | every bit flipped |
x << n | left shift | bits move n places left |
x >> n | right shift | bits move n places right |
Two spelling notes for readers arriving from C: Rust's bitwise NOT is !, not ~ (the same ! you know from bools; the operand's type decides which job it does), and ^ is XOR, never exponentiation, which is how lesson 6.2's 2 ^ 10 == 8 mystery happened.
The four combiners
AND, OR, and XOR all work column by column: line the two numbers up, apply the rule to each pair of bits independently, no carrying, no interaction between columns. NOT is unary and flips everything:
fn main() {
let a: u8 = 0b0000_0101;
let b: u8 = 0b0000_0110;
println!("a & b = {:08b}", a & b);
println!("a | b = {:08b}", a | b);
println!("a ^ b = {:08b}", a ^ b);
println!("!a = {:08b}", !a);
}a & b = 00000100
a | b = 00000111
a ^ b = 00000011
!a = 11111010
Work the rightmost column of a & b by hand: a ends in 1, b ends in 0, AND demands both, result 0. The column to its left: 0 and 1, still 0. Third column: 1 and 1, finally a 1. Doing this a few times on paper is how it sticks; the quiz will ask.
Now lesson 6.2's payoff. The mysterious 2 ^ 10:
0000_0010 (2)
0000_1010 (10)
--------- XOR: 1 where they differ
0000_1000 (8)
No drama, just an answer to a question nobody meant to ask.
The shifts
<< slides every bit toward the big end, filling with zeros from the right; >> slides the other way. Bits pushed off the edge are gone.
fn main() {
let x: u8 = 0b0000_1100;
println!("{:08b}", x << 2);
println!("{:08b}", x >> 2);
println!("{:08b}", x << 5);
}00110000
00000011
10000000
The third line shows the falling-off: shifted by 5, the higher of the two 1-bits ran off the left edge and was lost. Numerically, each left shift doubles a number and each right shift halves it (truncating), which is why compilers love shifts; you'll mostly use them for placing bits, as the next section shows.
Warning
Shifting by the type's full width or more (a u8 by 8, say) is an overflow, with lesson 4.4's debug-build consequences: a panic ("attempt to shift left with overflow"). And if the compiler can prove it from literals, it refuses to build at all, just like 6.2's division by zero. Keep shift amounts under the bit width.
For advanced readers
On signed integers, >> copies the sign bit in from the left instead of zeros (an "arithmetic" shift), preserving negativity at the cost of surprises. This is one of two reasons bit work conventionally uses unsigned types; the other is that ! and overflow behave more predictably without a sign bit in the way.
All six operators have compound-assignment forms (&=, |=, ^=, <<=, >>=), completing the table lesson 6.3 promised.
Bit flags: eight booleans in a trench coat
Here's the trade that makes all of this practical. A bool spends a whole byte storing one yes/no. A u8 can store eight yes/nos, if you're willing to address them with operators instead of names. The idioms:
const BOLD: u8 = 1 << 0; // 0000_0001
const ITALIC: u8 = 1 << 1; // 0000_0010
const UNDERLINE: u8 = 1 << 2; // 0000_0100
const STRIKETHROUGH: u8 = 1 << 3; // 0000_1000
fn main() {
let mut style: u8 = 0;
style |= BOLD; // set a flag
style |= ITALIC | UNDERLINE; // set several at once
style &= !UNDERLINE; // clear a flag
style ^= STRIKETHROUGH; // toggle a flag
println!("style: {style:08b}");
println!("bold? {}", style & BOLD != 0);
println!("italic? {}", style & ITALIC != 0);
println!("under? {}", style & UNDERLINE != 0);
println!("strike? {}", style & STRIKETHROUGH != 0);
}style: 00001011
bold? true
italic? true
under? false
strike? true
Each constant is a bit mask: a value with exactly one bit set, built with a shift, marking which switch it owns (1 << 0 is the constant-friendly way to write "bit zero"; lesson 5.1's const handles the arithmetic at compile time). Then four idioms do everything:
| Intent | Idiom | Why it works |
|---|---|---|
| set | flags |= MASK | OR turns the masked bit on, leaves the rest alone |
| clear | flags &= !MASK | NOT makes a mask of everything-except, AND keeps only those |
| toggle | flags ^= MASK | XOR flips every bit where the mask has a 1, leaves the rest alone |
| test | flags & MASK != 0 | AND isolates the one bit; any nonzero result means it was set |
(That last idiom works unparenthesized because Rust ranks & above comparisons in lesson 6.1's table. C ranks it below, a historical accident its own creators regretted, and every C programmer parenthesizes there out of scar tissue.)
This is not an exotic technique. Unix file permissions are nine bit flags (rwxrwxrwx). Graphics APIs take flag arguments like WINDOW_RESIZABLE | WINDOW_FULLSCREEN. Network protocol headers are bit fields end to end. When you meet those, they'll look exactly like this lesson.
Quiz time
Question #1
Evaluate by hand (all values are u8, answers in 8-bit binary):
a) 0b0000_0110 >> 2
b) 0b0000_0011 | 0b0000_0101
c) 0b0000_0011 & 0b0000_0101
d) (0b0000_0011 | 0b0000_0101) & 0b0000_1001
Show solution
a) 00000001 (both bits slide right two places; the low 1 falls off)
b) 00000111 (either)
c) 00000001 (both: only the rightmost column has two 1s)
d) 00000001 (the OR gives 0000_0111, AND with 0000_1001 keeps only the bottom bit)
Question #2
Using the constants from the lesson, write one line each, then give style in binary after all three (starting from let mut style: u8 = BOLD | ITALIC;):
a) turn on STRIKETHROUGH
b) turn off BOLD
c) flip ITALIC
Show solution
style |= STRIKETHROUGH;
style &= !BOLD;
style ^= ITALIC;
Start: 0000_0011. After (a): 0000_1011. After (b): 0000_1010. After (c), the italic bit (currently 1) flips off: 0000_1000. Only strikethrough survives.
Question #3
Lesson 6.5's De Morgan's laws were stated for bools. Do they hold for bits? Explain why !(BOLD | ITALIC) and !BOLD & !ITALIC are the same mask.
Show solution
They hold, column by column. Each bit position is an independent tiny boolean, and bitwise NOT/AND/OR apply the boolean rules to every column at once, so any identity true of bools is true of each column and therefore of the whole value. Concretely: BOLD | ITALIC is 0000_0011, NOT gives 1111_1100. The other way: !BOLD is 1111_1110, !ITALIC is 1111_1101, and their AND is 1111_1100. Same mask, both readings: "neither bold nor italic".
Whether you worked through this lesson or skipped to here: the chapter summary and quiz is next, and it closes out the operators.