A bit mask is an integer whose 1 bits mark the positions you care about. Combined with the right operator, a mask lets you read, set, clear, or toggle those positions without disturbing the rest of the value. Almost every piece of low-level code, from device drivers to game engines to database engines, relies on these four operations.

This guide walks through each operation, then builds a small permission system that uses all four.

The Four Mask Operations

GoalExpressionOperator used
Read (test) bitsvalue & MASKAND
Set bits to 1value | MASKOR
Clear bits to 0value & ~MASKAND + NOT
Toggle bitsvalue ^ MASKXOR

Each row follows from the operator’s truth table. AND with 1 keeps a bit and AND with 0 clears it. OR with 1 sets a bit and OR with 0 keeps it. XOR with 1 flips a bit and XOR with 0 keeps it. In every case, the mask’s 0 bits leave the value alone. That is what makes masks safe to apply.

Building Masks With Shifts

Hard-coding masks such as 0x20 works, but it hides the intent. Building them from a bit position is clearer:

#define BIT(n) (1u << (n))

BIT(0)  /* 0b00000001 = 0x01 */
BIT(5)  /* 0b00100000 = 0x20 */
BIT(7)  /* 0b10000000 = 0x80 */

For a run of bits, use ((1u << width) - 1) << offset. A 3-bit field starting at bit 4 is ((1u << 3) - 1) << 4 = 0b0111_0000 = 0x70. Always use unsigned types in C and C++ for masks. Shifting a 1 into the sign bit of a signed int is undefined behaviour.

Reading Bits With AND

To test one flag, AND with its mask and compare against zero:

if (flags & BIT(3)) { /* bit 3 is set */ }

To read a multi-bit field, shift it down and mask it:

unsigned mode = (reg >> 4) & 0x7;   /* bits 4..6 */

The order matters. Shifting first means the mask is always the small value 0x7, regardless of where the field lives. You can verify any of these with the Bitwise AND Calculator: enter the register value and the mask in hex and read the result.

Setting, Clearing, and Toggling

flags |=  BIT(2);   /* set bit 2    */
flags &= ~BIT(2);   /* clear bit 2  */
flags ^=  BIT(2);   /* toggle bit 2 */

To write a new value into a multi-bit field, clear the field first and then OR in the new bits:

reg = (reg & ~(0x7u << 4)) | ((mode & 0x7u) << 4);

Skipping the clear step is one of the most common bugs in register code. Old bits that were 1 stay 1, and the field ends up as a mix of old and new values.

Worked Example: A Permission System

Imagine an app with five permissions stored in one byte:

BitPermissionMask
0view0x01
1comment0x02
2edit0x04
3delete0x08
4admin0x10

A user who can view, comment, and edit has the permission value 0x01 | 0x02 | 0x04 = 0x07.

VIEW, COMMENT, EDIT, DELETE, ADMIN = 1, 2, 4, 8, 16

perms = VIEW | COMMENT | EDIT           # 7  (0b00111)
can_edit = bool(perms & EDIT)           # True
perms |= DELETE                         # 15 (0b01111)  grant delete
perms &= ~COMMENT                       # 13 (0b01101)  revoke comment
perms ^= ADMIN                          # 29 (0b11101)  toggle admin

# Does the user have BOTH edit and delete?
needed = EDIT | DELETE
has_all = (perms & needed) == needed    # True

# Does the user have ANY of edit or delete?
has_any = (perms & needed) != 0         # True

The last two checks are worth memorising. (perms & needed) == needed means all required bits are present. (perms & needed) != 0 means at least one is present.

The same logic in JavaScript:

const VIEW = 1, COMMENT = 2, EDIT = 4, DELETE = 8, ADMIN = 16;
let perms = VIEW | COMMENT | EDIT;
perms &= ~COMMENT;
console.log(perms.toString(2).padStart(5, "0")); // "00101"

Storing permissions this way keeps them in a single database column and makes checks extremely fast. The trade-off is that a 32-bit integer only holds 32 flags, and adding a new permission means agreeing on a new bit position. For a deeper look at that trade-off, read Feature Flags With Bitfields.

Debugging Masks

When a mask doesn’t behave as expected, check these in order:

  1. Print in binary or hex, not decimal. 0b00101101 tells you far more than 45.
  2. Check precedence. In C-family languages, flags & MASK == MASK is parsed as flags & (MASK == MASK). Add parentheses.
  3. Check the width. ~0x0F is 0xF0 in 8 bits but 0xFFFFFFF0 in 32 bits. Use the Bitwise NOT Calculator at your type’s width.
  4. Check signedness. In JavaScript, bit 31 makes the result negative. Use >>> 0 to view it unsigned.

Summary

Masks turn one integer into a compact set of switches. Use AND to read and clear, OR to set, XOR to toggle, and NOT to invert a mask. Build masks from bit positions, clear a field before writing it, and print values in binary while debugging. When in doubt, paste the values into the calculator and look at the bit grid.