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 — implementation-defined for negatives before C++20; well-defined arithmetic (sign-extending) shift since C++20, = floor(E1 / 2^E2). For pre-C++20 portable code, prefer unsigned.
  2. Shifting by >= width — UB.
  3. Precedence trap&, |, ^ have lower precedence than == / !=. Always parenthesize: (x & MASK) == 0, not x & MASK == 0.

2. std::bitset

  • std::bitset<N> (<bitset>) — fixed-size N-bit sequence with convenient ops.
#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
  • Type-safe (no signed/unsigned mix), self-describing in print.
  • Dynamic size — use std::vector<bool> (1 bit per element) or boost::dynamic_bitset.

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

  • <bit> (C++20) — low-level bit ops 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) {
    // ...
}
  • Compile down to single CPU instructions (BMI/BMI2 on x86, dedicated ARM ops). Hand-rolled equivalents are slower + error-prone.

Use cases:

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

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

  • std::bit_cast<T>(x) — reinterpret bits of x as type T. Replaces reinterpret_cast + memcpy 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 trivially copyable.

vs alternatives:

  1. constexpr since C++20 — usable at compile time (when neither type contains unions, pointers, member pointers, references, or volatile members).
  2. No strict-aliasing violation — produces a fresh object.
  3. reinterpret_cast + memcpy works at runtime but isn't constexpr.
  • Standard tool for float bit-fiddling, packing structs into ints, hashing raw representations. See also K2A-B1-10 § 7.

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

  • std::byteswap — reverse byte order. For endian conversion (network ↔ host).
#include <bit>
#include <cstdint>

std::uint32_t x = 0x12345678;
std::uint32_t swapped = std::byteswap(x);   // 0x78563412
  • Replaces POSIX htonl / ntohl and __builtin_bswap32 extensions with portable, type-generic, constexpr standard.
// 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
}
  • x & (x - 1) clears the lowest set bit; combined with countr_zero, walks 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 — unspecified layout (no portable bit ordering or packing). For wire formats, prefer manual masks + shifts.