Bit flags look a bit cryptic the first time you see them. There are shifts, ampersands, pipes, and somehow a single number is holding a bunch of true and false values.
But the idea is actually pretty simple.
Let's say a user can have four permissions:
Read = 00000001
Write = 00000010
Delete = 00000100
Share = 00001000
A u8 has eight bits, and each permission owns one of them. A 1 means enabled, a 0 means disabled.
The values above come from shifting a single bit to a different position:
1 << 0 = 00000001
1 << 1 = 00000010
1 << 2 = 00000100
1 << 3 = 00001000
Now let's say someone has Read and Delete permissions. Put those two together and you get:
Read: 00000001
Delete: 00000100
--------
Both: 00000101
That's it. 00000101 is just a normal number, but we choose to read its bits as a set of switches.
Why not just use four booleans?
Honestly, sometimes you should.
If these are just four unrelated settings inside your application, four booleans are easier to read and harder to misuse. Bit flags become useful when the values naturally belong together and need to travel as one thing: a permissions mask, a database column, a network packet, an FFI boundary, or some low-level API.
They give you a compact and predictable representation, and make it easy to pass around or check several options at once. So this isn't a trick you should use everywhere. It fits when the data really is a set of on/off switches.
Changing the switches
There are four operations we care about:
Has flags & target
Enable flags | target
Disable flags & !target
Toggle flags ^ target
AND (&) checks a flag. It keeps only bits that exist on both sides:
flags: 00000101
Delete: 00000100
--------
result: 00000100
The result still contains Delete, so that permission is enabled.
OR (|) enables a flag. If either side contains the bit, it ends up in the result:
flags: 00000001
Write: 00000010
--------
result: 00000011
AND NOT (& !) disables one. Inverting the target gives us zero at the exact position we want to clear. Everything else is left alone.
XOR (^) toggles one. An enabled bit becomes disabled and a disabled bit becomes enabled.
In Rust, a small wrapper around u8 is enough:
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PermissionFlags(u8);
impl PermissionFlags {
pub const NONE: Self = Self(0);
pub const READ: Self = Self(1 << 0);
pub const WRITE: Self = Self(1 << 1);
pub const DELETE: Self = Self(1 << 2);
pub const SHARE: Self = Self(1 << 3);
pub fn contains(self, target: Self) -> bool {
self.0 & target.0 == target.0
}
pub fn enable(&mut self, target: Self) {
self.0 |= target.0;
}
pub fn disable(&mut self, target: Self) {
self.0 &= !target.0;
}
pub fn toggle(&mut self, target: Self) {
self.0 ^= target.0;
}
}
Usage looks like this:
let mut permissions = PermissionFlags::NONE;
permissions.enable(PermissionFlags::READ);
permissions.enable(PermissionFlags::WRITE);
assert!(permissions.contains(PermissionFlags::READ));
assert!(permissions.contains(PermissionFlags::WRITE));
assert!(!permissions.contains(PermissionFlags::DELETE));
One small detail worth noticing is the contains check:
self.0 & target.0 == target.0
You will sometimes see != 0 instead. That works when checking one flag, but gives the wrong answer for a combination if even one of the requested flags exists. Comparing against target makes sure all of them are there.
For production code, the bitflags crate provides this pattern as a macro, including the useful operators and trait implementations.
And that's basically the whole concept. One number, a row of tiny on/off switches, and a few operators to flip exactly the ones you care about.