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 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. - Shifting by
>= widthof the type is undefined behavior. - Operator precedence:
&,|,^have lower precedence than==and!=. Always parenthesize:(x & MASK) == 0, notx & 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:
popcount— Hamming weight, set cardinality, bloom filter size.bit_ceil— round up to next power of 2 for hash table sizes, ring buffers.countl_zero— log2 of an integer (31 - countl_zero(x)), MSB index.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:
sizeof(T) == sizeof(U).- Both types are trivially copyable.
Compared to alternatives:
bit_castisconstexprsince C++20 — usable at compile time when neither type contains union members, pointers, pointer-to-member, references, or volatile-qualified subobjects.bit_castdoesn't violate strict aliasing (it produces a fresh object).reinterpret_cast+memcpyworks at runtime but isn'tconstexpr.
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.