Bitwise Operators
Bitwise operators manipulate the individual binary digits (bits) of integer values directly. Rust supports six bitwise operators on all signed and unsigned integer types (u8..u128, usize, i8..i128, isize): bitwise NOT (!), AND (&), OR (|), XOR (^), left shift (<<), and right shift (>>).
Overview of Bitwise Operators
| Operator | Name | Syntax | Bit-Level Behavior |
|---|---|---|---|
! |
Bitwise NOT (complement) | !x |
Flips every bit (0 becomes 1, 1 becomes 0) |
& |
Bitwise AND | a & b |
Result bit is 1 only if both corresponding bits are 1 |
| |
Bitwise OR (inclusive OR) | a | b |
Result bit is 1 if at least one corresponding bit is 1 |
^ |
Bitwise XOR (exclusive OR) | a ^ b |
Result bit is 1 if the corresponding bits are different |
<< |
Left shift | x << n |
Shifts bits left by n positions, filling low bits with 0 |
>> |
Right shift | x >> n |
Shifts bits right by n positions (0-fill for unsigned; sign-fill for signed) |
Note: Rust uses
!for bitwise NOT rather than C's tilde (~) operator. Onbool,!performs logical negation; on integers,!inverts every bit.
Bit-by-Bit Truth Table
For each bit position $i$, &, |, ^, and ! follow this truth table:
Bit a |
Bit b |
!a |
a & b |
a | b |
a ^ b |
|---|---|---|---|---|---|
0 |
0 |
1 |
0 |
0 |
0 |
0 |
1 |
1 |
0 |
1 |
1 |
1 |
0 |
0 |
0 |
1 |
1 |
1 |
1 |
0 |
1 |
1 |
0 |
Visualizing Bitwise Operations on u8
a = 0b1100_1010 (202)
b = 0b1010_1100 (172)
---------------------------
!a = 0b0011_0101 (53) <-- every bit of `a` inverted
a & b = 0b1000_1000 (136) <-- 1s only where both `a` and `b` have 1
a | b = 0b1110_1110 (238) <-- 1s where either `a` or `b` has 1
a ^ b = 0b0110_0110 (102) <-- 1s where `a` and `b` differ
fn main() {
let a: u8 = 0b1100_1010;
let b: u8 = 0b1010_1100;
println!("a = {:08b} ({a:3})", a);
println!("b = {:08b} ({b:3})", b);
println!("!a = {:08b} ({:3})", !a, !a);
println!("a & b = {:08b} ({:3})", a & b, a & b);
println!("a | b = {:08b} ({:3})", a | b, a | b);
println!("a ^ b = {:08b} ({:3})", a ^ b, a ^ b);
}
a = 11001010 (202)
b = 10101100 (172)
!a = 00110101 ( 53)
a & b = 10001000 (136)
a | b = 11101110 (238)
a ^ b = 01100110 (102)
Common Bitmask Idioms
Bitwise operators allow packing multiple boolean flags or compact fields into a single integer:
| Task | Idiom | Explanation |
|---|---|---|
Create mask for bit n |
1 << n |
Places a 1 at bit position n (0-indexed from the least significant bit) |
Set bit n to 1 |
flags |= 1 << n |
ORing with 1 sets bit n; ORing with 0 leaves other bits unchanged |
Clear bit n to 0 |
flags &= !(1 << n) |
ANDing with 0 at bit n clears it; 1s elsewhere preserve other bits |
Toggle (flip) bit n |
flags ^= 1 << n |
XORing with 1 inverts bit n; XORing with 0 preserves other bits |
Test if bit n is 1 |
(flags & (1 << n)) != 0 |
Isolates bit n and checks whether the result is non-zero |
const READ: u8 = 1 << 0; // 0b0000_0001 (1)
const WRITE: u8 = 1 << 1; // 0b0000_0010 (2)
const EXECUTE: u8 = 1 << 2; // 0b0000_0100 (4)
fn main() {
// 1. Combine flags with bitwise OR (`|`)
let mut perms: u8 = READ | WRITE;
println!("Initial (R+W): {:03b}", perms);
// 2. Set EXECUTE bit
perms |= EXECUTE;
println!("Added EXECUTE: {:03b}", perms);
// 3. Clear WRITE bit
perms &= !WRITE;
println!("Cleared WRITE: {:03b}", perms);
// 4. Toggle READ bit
perms ^= READ;
println!("Toggled READ: {:03b}", perms);
// 5. Test if EXECUTE is set
let can_exec = (perms & EXECUTE) != 0;
println!("Can execute? {can_exec}");
}
Initial (R+W): 011
Added EXECUTE: 111
Cleared WRITE: 101
Toggled READ: 100
Can execute? true
Left Shift (<<) and Right Shift (>>)
- Left shift (
x << n): Moves all bitsnpositions toward the most significant bit and fills the vacated low-order bits with0. Shifting left bynwithout overflow multipliesxby $2^n$. - Right shift (
x >> n): Moves all bitsnpositions toward the least significant bit. What fills the vacated high-order bits depends on whetherxis unsigned or signed:- Unsigned types (
u8..usize): Logical right shift—fills vacated left bits with0. - Signed types (
i8..isize): Arithmetic right shift—fills vacated left bits with copies of the sign bit (1if negative,0if non-negative).
- Unsigned types (
Logical Right Shift on u8 (0b1001_0100 >> 2):
0 0 -> [1 0 0 1 0 1 | 0 0] (dropped) ==> 0b0010_0101 (37)
Arithmetic Right Shift on i8 (-108 == 0b1001_0100 >> 2):
1 1 -> [1 0 0 1 0 1 | 0 0] (dropped) ==> 0b1110_0101 (-27)
fn main() {
let u: u8 = 0b1001_0100; // 148
let s: i8 = -108; // 0b1001_0100 in two's complement
println!("u << 1 = {:08b} ({})", u << 1, u << 1);
println!("u >> 2 = {:08b} ({})", u >> 2, u >> 2);
println!("s >> 2 = {:08b} ({})", (s >> 2) as u8, s >> 2);
}
u << 1 = 00101000 (40)
u >> 2 = 00100101 (37)
s >> 2 = 11100101 (-27)
Shift Count Types and Shift Overflow
Unlike &, |, and ^—which require both operands to have the exact same integer type—the right-hand operand (shift distance) of << and >> can be any integer type (for example, 1_u64 << 3_u8 is valid).
However, shifting by a negative number or by a number of bits greater than or equal to the bit width of the left operand (such as x << 8 on a u8 or x << 32 on a u32) is a shift overflow:
// ❌ does not compile
fn main() {
let x: u8 = 1;
let shifted = x << 8;
println!("{shifted}");
}
error: this arithmetic operation will overflow
--> src/main.rs:3:19
|
3 | let shifted = x << 8;
| ^^^^^^ attempt to shift left by `8_i32`, which would overflow
|
= note: `#[deny(arithmetic_overflow)]` on by default
error: aborting due to 1 previous error
When the shift distance is computed dynamically at runtime and may exceed the bit width, use .checked_shl(), .wrapping_shl(), or .overflowing_shl().
Built-In Bit Manipulation Methods
Every integer type in Rust provides intrinsic methods that compile directly to single CPU instructions (such as POPCNT, LZCNT, TZCNT, and ROR/ROL):
| Method | Example on 0b0001_1010_u8 |
Result | Description |
|---|---|---|---|
.count_ones() |
x.count_ones() |
3 |
Number of 1 bits (population count / Hamming weight) |
.count_zeros() |
x.count_zeros() |
5 |
Number of 0 bits |
.leading_zeros() |
x.leading_zeros() |
3 |
Consecutive 0 bits starting from the most significant bit |
.trailing_zeros() |
x.trailing_zeros() |
1 |
Consecutive 0 bits starting from the least significant bit |
.rotate_left(n) |
x.rotate_left(2) |
0b0110_1000 |
Circular bit shift left (bits shifted off the top wrap to the bottom) |
.rotate_right(n) |
x.rotate_right(2) |
0b1000_0110 |
Circular bit shift right |
.reverse_bits() |
x.reverse_bits() |
0b0101_1000 |
Reverses the order of all bits |
.swap_bytes() |
0x1234_u16.swap_bytes() |
0x3412_u16 |
Reverses byte order (endianness conversion) |
fn main() {
let mask: u8 = 0b0001_1010;
println!("ones: {}", mask.count_ones());
println!("leading_zeros: {}", mask.leading_zeros());
println!("trailing_zeros: {}", mask.trailing_zeros());
println!("rotate_left(4): {:08b}", mask.rotate_left(4));
println!("reverse_bits: {:08b}", mask.reverse_bits());
}
ones: 3
leading_zeros: 3
trailing_zeros: 1
rotate_left(4): 10100001
reverse_bits: 01011000
Precedence of Bitwise Operators
Within an expression, bitwise operators follow this precedence order from highest to lowest:
!(Bitwise NOT)+,-(Arithmetic addition and subtraction)<<,>>(Bit shifts)&(Bitwise AND)^(Bitwise XOR)|(Bitwise OR)==,!=,<,>,<=,>=(Comparisons)
Unlike C—where == and != bind tighter than &, ^, and |—Rust places all bitwise operators above comparison operators, so flags & mask != 0 evaluates (flags & mask) first. However, because arithmetic + and - bind tighter than << and & (meaning 1 << 2 + 1 is parsed as 1 << (2 + 1)), using explicit parentheses around bit shifts and masks (flags & mask) != 0 is standard practice.