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
- 2.
std::bitset - 3. The
<bit>Library (C++20) - 4.
std::bit_cast(C++20) - 5.
std::byteswap(C++23) - 6. Common Bit Manipulation Patterns
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:
- 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. - Shifting by
>= width— UB. - Precedence trap —
&,|,^have lower precedence than==/!=. Always parenthesize:(x & MASK) == 0, notx & 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) orboost::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:
popcount— Hamming weight, set cardinality, bloom filter size.bit_ceil— round up to next power of 2 (hash table sizes, ring buffers).countl_zero— log2 (31 - countl_zero(x)), MSB index.countr_zero— trailing zeros (some hash tables, CTZ-based iteration).
4. std::bit_cast (C++20)
std::bit_cast<T>(x)— reinterpret bits ofxas typeT. Replacesreinterpret_cast+memcpyfor 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:
sizeof(T) == sizeof(U).- Both types trivially copyable.
vs alternatives:
constexprsince C++20 — usable at compile time (when neither type contains unions, pointers, member pointers, references, or volatile members).- No strict-aliasing violation — produces a fresh object.
reinterpret_cast+memcpyworks at runtime but isn'tconstexpr.
- 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/ntohland__builtin_bswap32extensions with portable, type-generic,constexprstandard.
// 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 withcountr_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.