C++ Bit Operations


  • Description: A note on bitwise operators, std::bitset, the <bit> library (std::bit_cast, std::popcount, std::countl_zero, etc.), std::byteswap (C++23), and common bit-manipulation patterns
  • My Notion Note ID: K2A-B1-19
  • Created: 2020-04-05
  • Updated: 2026-02-28
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. Bitwise Operators

unsigned a = 0b1100;
unsigned b = 0b1010;

a & b;         // 0b1000 — AND
a | b;         // 0b1110 — OR
a ^ b;         // 0b0110 — XOR
~a;            // bitwise NOT (all bits flipped)
a << 2;        // 0b110000 — left shift (multiply by 2^2)
a >> 1;        // 0b110   — right shift (divide by 2)

// Compound forms
a &= b;
a |= b;
a ^= b;
a <<= 2;
a >>= 1;

Caveats:

  1. Right shift on signed integers was implementation-defined for negative values before C++20. Since C++20, it is well-defined as an arithmetic (sign-extending) shift, equivalent to floor(E1 / 2^E2). For pre-C++20 portable code, prefer unsigned types for bitwise work.
  2. Shifting by >= width of the type is undefined behavior.
  3. Operator precedence: &, |, ^ have lower precedence than == and !=. Always parenthesize: (x & MASK) == 0, not x & MASK == 0.

2. std::bitset

std::bitset<N> (<bitset>) is a fixed-size sequence of N bits with convenient operations.

#include <bitset>

std::bitset<8> b{0b10110001};      // 8 bits, set from binary literal
std::bitset<8> c{"10110001"};       // also from string

b.set(0);                           // set bit 0 to 1
b.reset(2);                         // set bit 2 to 0
b.flip(3);                          // toggle bit 3

b[0];                               // bit access (proxy)
b.test(7);                          // bounds-checked access
b.size();                           // 8
b.count();                          // number of set bits
b.any();                            // any set?
b.all();                            // all set?
b.none();                           // none set?

b.to_ulong();                       // convert to unsigned long
b.to_string();                      // "10110001"

b & c;                              // bitwise AND of two bitsets
b << 1;                             // shift

bitset is type-safe (you can't accidentally mix signed and unsigned) and self-describing in print. For dynamic-size bit arrays, use std::vector<bool> (specialized to one bit per element) or boost::dynamic_bitset.


3. The <bit> Library (C++20)

C++20 added <bit>, providing low-level bit operations as portable, optimized standard functions.

#include <bit>
#include <cstdint>

std::uint32_t x = 0b00101100;

std::popcount(x);           // 3 — number of 1 bits
std::has_single_bit(x);     // false — is x a power of 2?
std::bit_width(x);          // 6 — minimum bits to represent x (1-indexed: bit position of MSB + 1)
std::bit_floor(x);          // 32  — largest power of 2 ≤ x
std::bit_ceil(x);           // 64  — smallest power of 2 ≥ x

std::countl_zero(x);        // 26 — leading zeros (in a 32-bit type)
std::countl_one(x);         // 0
std::countr_zero(x);        // 2  — trailing zeros
std::countr_one(x);         // 0

std::rotl(x, 4);            // rotate left by 4
std::rotr(x, 4);            // rotate right by 4

// Endian detection
if constexpr (std::endian::native == std::endian::little) {
    // ...
}

These compile down to single CPU instructions on most architectures (BMI/BMI2 on x86, dedicated instructions on ARM). Manually written equivalents are slower and error-prone.

Use cases:

  1. popcount — Hamming weight, set cardinality, bloom filter size.
  2. bit_ceil — round up to next power of 2 for hash table sizes, ring buffers.
  3. countl_zero — log2 of an integer (31 - countl_zero(x)), MSB index.
  4. countr_zero — number of trailing zero bits (used in some hash tables, CTZ-based iteration).

4. std::bit_cast (C++20)

std::bit_cast<T>(x) reinterprets the bits of x as type T. It replaces the reinterpret_cast-plus-memcpy pattern for value-to-value bit reinterpretation.

#include <bit>
#include <cstdint>

float f = 1.5f;
std::uint32_t bits = std::bit_cast<std::uint32_t>(f);   // raw IEEE 754 bits: 0x3FC00000

// Reverse:
float back = std::bit_cast<float>(bits);                 // 1.5f

// Extract sign bit
constexpr std::uint32_t sign_bit = 0x80000000;
bool negative = std::bit_cast<std::uint32_t>(f) & sign_bit;

Requirements:

  1. sizeof(T) == sizeof(U).
  2. Both types are trivially copyable.

Compared to alternatives:

  1. bit_cast is constexpr since C++20 — usable at compile time when neither type contains union members, pointers, pointer-to-member, references, or volatile-qualified subobjects.
  2. bit_cast doesn't violate strict aliasing (it produces a fresh object).
  3. reinterpret_cast + memcpy works at runtime but isn't constexpr.

This is the standard tool for bit-level work on floats, packing structs into integers, hashing on raw representations, etc. See also K2A-B1-10 § 7.


5. std::byteswap (C++23)

std::byteswap reverses the byte order of an integer — useful for endian conversion (network byte order ↔ host byte order).

#include <bit>
#include <cstdint>

std::uint32_t x = 0x12345678;
std::uint32_t swapped = std::byteswap(x);   // 0x78563412

Replaces the older htonl / ntohl POSIX functions and assorted __builtin_bswap32 extensions with a portable, type-generic, constexpr standard function.

// Endian conversion:
std::uint32_t to_big_endian(std::uint32_t v) {
    if constexpr (std::endian::native == std::endian::little) {
        return std::byteswap(v);
    } else {
        return v;
    }
}

6. Common Bit Manipulation Patterns

Set, clear, toggle, test a bit

unsigned x = 0;

x |=  (1u << bit);      // set bit
x &= ~(1u << bit);      // clear bit
x ^=  (1u << bit);      // toggle bit
bool isSet = x & (1u << bit);  // test bit

Check power of 2

bool is_pow2(unsigned x) {
    return x != 0 && (x & (x - 1)) == 0;
}
// Or, in C++20:
std::has_single_bit(x);

Round up to next power of 2

unsigned next_pow2_classic(unsigned x) {
    if (x == 0) return 1;
    --x;
    x |= x >> 1;
    x |= x >> 2;
    x |= x >> 4;
    x |= x >> 8;
    x |= x >> 16;
    return x + 1;
}
// C++20:
std::bit_ceil(x);

Iterate over set bits

for (unsigned x = mask; x != 0; x &= (x - 1)) {
    int bit = std::countr_zero(x);   // lowest set bit
    // process bit
}

The x & (x - 1) trick clears the lowest set bit; combined with countr_zero, you can walk a sparse bitmask in O(popcount) time.

Bit field packing

struct Packed {
    unsigned color : 8;       // 8 bits
    unsigned alpha : 8;       // 8 bits
    unsigned reserved : 16;   // 16 bits
};
// sizeof(Packed) == 4 (typically)

Bit fields are convenient but have unspecified layout (no portable bit ordering, packing). For wire formats, prefer manual masks-and-shifts over bit fields.