6.6Bitwise operators (optional)

Last updated June 12, 2026

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:

OperatorNameEffect
x & ybitwise AND1 where both bits are 1
x | ybitwise OR1 where either bit is 1
x ^ ybitwise XOR1 where the bits differ
!xbitwise NOTevery bit flipped
x << nleft shiftbits move n places left
x >> nright shiftbits 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:

IntentIdiomWhy it works
setflags |= MASKOR turns the masked bit on, leaves the rest alone
clearflags &= !MASKNOT makes a mask of everything-except, AND keeps only those
toggleflags ^= MASKXOR flips every bit where the mask has a 1, leaves the rest alone
testflags & MASK != 0AND 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.