Bit Manipulation
Bitwise operators, std::bitset, and C++20's <bit> header — popcount, rotl, countl_zero, std::bit_cast, endianness detection, and portable bit tricks.
Bit Manipulationsince C++98Direct manipulation of integer values at the bit level using bitwise operators, std::bitset, and — since C++20 — the <bit> header of portable, constexpr-safe functions that replace compiler intrinsics.
Overview
C++ has carried bitwise operators from its C lineage, but writing portable, well-defined bit code was historically error-prone: __builtin_popcount existed in GCC/Clang but not MSVC; right-shifting a negative signed integer was implementation-defined; uint8_t operands silently promoted to int before the operator fired.
C++20 addressed this on two fronts. First, it mandated two's-complement representation for signed integers (previously implementation-defined), making signed-bit arithmetic predictable across platforms. Second, the <bit> header introduced a suite of constexpr functions — std::popcount, std::rotl, std::countl_zero, std::bit_cast, and more — that work exclusively on unsigned integer types, turning what was previously a grab-bag of non-portable intrinsics into standardized vocabulary. C++23 added std::byteswap.
Syntax
Bitwise Operators (C++98)
uint32_t a = 0b1100u, b = 0b1010u; // binary literals: C++14
a & b // 0b1000 — AND: bits set in both
a | b // 0b1110 — OR: bits set in either
a ^ b // 0b0110 — XOR: bits set in exactly one
~a // bitwise NOT (all bits flipped)
a << 2 // 0b110000 — left shift: multiply by 4
a >> 1 // 0b0110 — right shift (unsigned: logical shift, zero-fill)
// Digit separators (C++14) aid readability:
uint32_t mask = 0b1010'0101'1100'0011u;
// Compound assignment forms (C++98):
a &= b; a |= b; a ^= b; a <<= 2; a >>= 1;Right-shifting an unsigned type always zero-fills from the MSB (logical shift). For signed types: left-shift is defined as modular in C++20; right-shift of a negative value is arithmetic (fills with the sign bit) — this is implementation-defined in C++17 and earlier, but practically universal. Avoid signed types for bit manipulation.
C++20 <bit> Functions — Complete API
All functions in <bit> are constexpr, reject signed types at compile time, and compile to single hardware instructions under optimization on all major architectures.
#include <bit> // C++20
#include <cstdint>
// ── Population count ──────────────────────────────────────────────────────
// Returns the number of 1-bits in the value.
std::popcount(uint32_t{0b0110'1001u}) // 4
std::popcount(uint16_t{0xFF}) // 8
std::popcount(uint64_t{0}) // 0
// ── Has single bit (exact power of two) ───────────────────────────────────
// True iff exactly one bit is set (equivalent to n != 0 && (n & (n-1)) == 0).
std::has_single_bit(uint32_t{64}) // true — 0b0100'0000
std::has_single_bit(uint32_t{0}) // false — 0 has no set bits
std::has_single_bit(uint32_t{3}) // false — 0b11 has two set bits
// ── Bit width ─────────────────────────────────────────────────────────────
// Returns 1 + floor(log2(n)) for n > 0; 0 for n == 0.
// Equivalently: minimum number of bits to represent n.
std::bit_width(uint32_t{0}) // 0
std::bit_width(uint32_t{1}) // 1
std::bit_width(uint32_t{7}) // 3 (0b111 needs 3 bits)
std::bit_width(uint32_t{100}) // 7 (2^6=64 < 100 ≤ 2^7=128)
std::bit_width(uint32_t{128}) // 8 (2^7=128, needs 8 bits to fit)
// ── Round to power of two ─────────────────────────────────────────────────
// bit_ceil: smallest power of 2 >= n. UB if result is not representable.
std::bit_ceil(uint32_t{1}) // 1 (already a power of two)
std::bit_ceil(uint32_t{5}) // 8
std::bit_ceil(uint32_t{8}) // 8
std::bit_ceil(uint32_t{9}) // 16
// bit_floor: largest power of 2 <= n. Returns 0 for n == 0.
std::bit_floor(uint32_t{0}) // 0 (special case)
std::bit_floor(uint32_t{1}) // 1
std::bit_floor(uint32_t{9}) // 8
std::bit_floor(uint32_t{8}) // 8
// ── Leading/trailing zero counts ──────────────────────────────────────────
// countl_zero: number of consecutive 0-bits starting from the MSB.
// Returns the full bit-width of the type when the value is 0.
std::countl_zero(uint32_t{1}) // 31 — only bit 0 is set
std::countl_zero(uint32_t{0x80000000u}) // 0 — MSB is set
std::countl_zero(uint32_t{0}) // 32 — all zeros
std::countl_zero(uint8_t{0b00011000}) // 3
// countr_zero: number of consecutive 0-bits starting from the LSB.
std::countr_zero(uint32_t{8}) // 3 (8 = 0b1000; three trailing zeros)
std::countr_zero(uint32_t{1}) // 0
std::countr_zero(uint32_t{0}) // 32
// ── Leading/trailing one counts ───────────────────────────────────────────
// countl_one: consecutive 1-bits from MSB.
std::countl_one(uint8_t{0b11100000u}) // 3
std::countl_one(uint8_t{0xFF}) // 8
std::countl_one(uint8_t{0x7F}) // 0 — MSB is 0
// countr_one: consecutive 1-bits from LSB.
std::countr_one(uint8_t{0b00000111u}) // 3
std::countr_one(uint8_t{0xFF}) // 8
std::countr_one(uint8_t{0xFE}) // 0 — LSB is 0
// ── Rotation ──────────────────────────────────────────────────────────────
// Bits rotate through the type width; no bits are lost.
// Negative shift count reverses direction (rotl(-n) == rotr(n)).
std::rotl(uint8_t{0b00001111u}, 2) // 0b00111100 — shifted left, wrap to right
std::rotr(uint8_t{0b11110000u}, 2) // 0b00111100 — shifted right, wrap to left
std::rotl(uint8_t{0b10000001u}, 1) // 0b00000011 — MSB wraps to bit 0
std::rotl(uint32_t{1u}, 31) // 0x80000000uExamples
Core Bit Tricks
#include <bit>
#include <cstdint>
bool test_bit(uint32_t x, int n) { return (x >> n) & 1u; }
uint32_t set_bit(uint32_t x, int n) { return x | (1u << n); }
uint32_t clear_bit(uint32_t x, int n) { return x & ~(1u << n); }
uint32_t toggle_bit(uint32_t x, int n) { return x ^ (1u << n); }
// Contiguous field extraction [hi:lo] inclusive
uint32_t extract_bits(uint32_t x, int lo, int hi) {
return (x >> lo) & ((1u << (hi - lo + 1)) - 1u);
}
// Isolate / clear the lowest set bit
uint32_t lsb(uint32_t x) { return x & -x; } // -x wraps (unsigned arithmetic)
uint32_t clear_lsb(uint32_t x) { return x & (x - 1u); }
// Integer floor-log2 — portable C++20
int ilog2(uint32_t n) { return static_cast<int>(std::bit_width(n)) - 1; }std::bit_cast — Safe Type Punning (C++20)
Union and reinterpret_cast-through-pointer type punning is undefined behavior under strict aliasing. std::bit_cast is the constexpr-safe replacement; it requires sizeof(To) == sizeof(From) and both types trivially copyable:
#include <bit>
#include <cstdint>
constexpr uint32_t float_bits(float f) noexcept {
return std::bit_cast<uint32_t>(f); // C++20
}
constexpr bool float_is_negative(float f) noexcept {
return (float_bits(f) >> 31u) == 1u;
}
static_assert(float_bits(0.0f) == 0x00000000u);
static_assert(float_bits(-1.0f) == 0xBF800000u);
static_assert(float_is_negative(-3.14f));The classic "fast inverse square root" from Quake III used a pointer cast to read a float as a uint32_t. That code is undefined behavior under C++ strict-aliasing rules. With std::bit_cast, it becomes well-defined and constexpr-eligible:
#include <bit>
#include <cstdint>
// Quake's fast inverse square root — safely rewritten with bit_cast (C++20).
// Original used: i = *(uint32_t*)&y; // UB: violates strict aliasing
float fast_inv_sqrt(float number) noexcept {
float y = number;
float x2 = number * 0.5f;
uint32_t i = std::bit_cast<uint32_t>(y); // safe: same size, trivially copyable
i = 0x5f3759dfu - (i >> 1); // bit-level Newton's method seed
y = std::bit_cast<float>(i); // convert back
y = y * (1.5f - (x2 * y * y)); // one Newton-Raphson iteration
return y;
}
// std::bit_cast constraints (checked at compile time):
// sizeof(To) == sizeof(From) — float and uint32_t are both 4 bytes
// std::is_trivially_copyable_v<To> — uint32_t: yes
// std::is_trivially_copyable_v<From> — float: yesstd::byteswap — Byte Reversal (C++23)
std::byteswap reverses the byte order of an integer in a single expression. It compiles to the bswap instruction on x86, rev on ARM, and equivalent single-cycle hardware operations on other major architectures.
#include <bit> // C++23
#include <cstdint>
static_assert(std::byteswap(uint32_t{0x12345678u}) == 0x78563412u);
static_assert(std::byteswap(uint16_t{0xABCDu}) == 0xCDABu);
static_assert(std::byteswap(uint64_t{0x0102030405060708ULL})
== 0x0807060504030201ULL);
static_assert(std::byteswap(uint8_t{0xFFu}) == 0xFFu); // no-op for single byteThe primary use case is converting between host byte order and network (big-endian) byte order when parsing binary protocols:
#include <bit>
#include <cstdint>
#include <cstring> // memcpy
// Read a big-endian uint32 from a network packet on a little-endian machine.
// Protocol spec says bytes arrive MSB first (big-endian / network byte order).
uint32_t read_be_u32(const uint8_t* buf) noexcept {
uint32_t raw{};
std::memcpy(&raw, buf, 4); // safe byte copy, avoids alignment UB
if constexpr (std::endian::native == std::endian::little)
return std::byteswap(raw); // C++23
return raw;
}
// Write a host-order value into a big-endian packet field.
void write_be_u32(uint8_t* buf, uint32_t host_val) noexcept {
uint32_t net_val = host_val;
if constexpr (std::endian::native == std::endian::little)
net_val = std::byteswap(net_val); // C++23
std::memcpy(buf, &net_val, 4);
}std::endian — Portable Endianness Detection (C++20)
std::endian is a scoped enum with three enumerators: little, big, and native. On a little-endian platform native == little; on big-endian native == big. On a mixed-endian platform (rare today) all three may differ.
#include <bit> // C++20
// Compile-time branch — zero runtime overhead.
if constexpr (std::endian::native == std::endian::little) {
// x86, x86-64, ARM (typical LE configuration), RISC-V LE
} else if constexpr (std::endian::native == std::endian::big) {
// SPARC, MIPS (BE mode), s390x, network byte order machines
} else {
// Mixed/unusual endianness — add explicit handling
}Portable endian-swap that is a no-op on big-endian hosts and a single bswap on little-endian, with a C++20-compatible fallback for when C++23 byteswap is unavailable:
#include <bit>
#include <cstdint>
// C++20-compatible manual swap (no std::byteswap yet):
constexpr uint32_t bswap32(uint32_t n) noexcept {
return ((n & 0xFF000000u) >> 24u)
| ((n & 0x00FF0000u) >> 8u)
| ((n & 0x0000FF00u) << 8u)
| ((n & 0x000000FFu) << 24u);
}
// Generic "to big-endian" helper — use std::byteswap on C++23 targets.
constexpr uint32_t to_big_endian(uint32_t host) noexcept {
if constexpr (std::endian::native == std::endian::big)
return host; // already big-endian, nothing to do
else
return bswap32(host); // swap on little-endian (C++20)
// return std::byteswap(host); // preferred on C++23
}
constexpr uint32_t from_big_endian(uint32_t net) noexcept {
return to_big_endian(net); // same operation: swap is its own inverse
}std::bitset — Named Flag Sets (C++98)
std::bitset<N> provides a fixed-size array of bits with string I/O, flip/test/set/reset operations, and bitwise operators between sets of the same size. N is a compile-time constant.
#include <bitset>
#include <iostream>
#include <string>
// Construction
std::bitset<8> a; // all zeros: 00000000
std::bitset<8> b{0b10110100u}; // from unsigned long
std::bitset<8> c{"10110100"}; // from string (MSB first in string)
std::bitset<8> d{"xyxyyxyy", 8, 'x', 'y'}; // custom zero/one chars
// Element access and modification
std::bitset<8> flags;
flags[3] = true; // set bit 3 via reference proxy
flags.set(5); // set bit 5
flags.set(7); // set bit 7
flags.reset(5); // clear bit 5
flags.flip(3); // toggle bit 3 — now 0 again
flags.flip(); // flip all bits
// Queries
std::bitset<8> perms{0b00000101u}; // bits 0 and 2 set
perms.test(0); // true — throws std::out_of_range if index >= N
perms[0]; // true — no bounds check
perms.any(); // true — at least one bit set
perms.none(); // false — not all zero
perms.all(); // false — not all one
perms.count(); // 2 — number of set bits
perms.size(); // 8 — always N (compile-time constant)
// Conversion
uint64_t raw = perms.to_ulong(); // 5 (throws std::overflow_error if N > 64)
std::string s = perms.to_string(); // "00000101" (MSB first)
// Bitwise operators between same-size bitsets
std::bitset<8> x{0b11001100u}, y{0b10101010u};
auto combined = x | y; // 0b11101110
auto shared = x & y; // 0b10001000
auto diff = x ^ y; // 0b01100110
auto flipped = ~x; // 0b00110011A practical flag-set example — permission bits for a filesystem entry:
#include <bitset>
enum PermBit : size_t {
OtherExec = 0,
OtherWrite = 1,
OtherRead = 2,
GroupExec = 3,
GroupWrite = 4,
GroupRead = 5,
OwnerExec = 6,
OwnerWrite = 7,
OwnerRead = 8,
};
std::bitset<9> perm;
perm.set(OwnerRead);
perm.set(OwnerWrite);
perm.set(GroupRead);
perm.set(OtherRead);
// Unix octal 0644 == 110 100 100 binary
std::cout << perm.to_string() << '\n'; // "110100100"
std::cout << std::oct << perm.to_ulong() << '\n'; // 644Bitmask Enums
enum class prevents accidental integer mixing but suppresses bitwise operators by default. Add them explicitly as constexpr non-members:
enum class Perm : uint32_t {
None = 0,
Read = 1u << 0,
Write = 1u << 1,
Execute = 1u << 2,
All = Read | Write | Execute,
};
constexpr Perm operator|(Perm a, Perm b) noexcept {
return static_cast<Perm>(static_cast<uint32_t>(a) | static_cast<uint32_t>(b));
}
constexpr Perm operator&(Perm a, Perm b) noexcept {
return static_cast<Perm>(static_cast<uint32_t>(a) & static_cast<uint32_t>(b));
}
constexpr Perm operator~(Perm a) noexcept {
return static_cast<Perm>(~static_cast<uint32_t>(a));
}
constexpr bool has_perm(Perm set, Perm flag) noexcept {
return (set & flag) == flag;
}
constexpr auto rw = Perm::Read | Perm::Write;
static_assert(has_perm(rw, Perm::Read));
static_assert(!has_perm(rw, Perm::Execute));Common Bit Tricks and Their C++20 Equivalents
These idioms predate the <bit> header and appear widely in legacy code. The table pairs each trick with its standard equivalent.
#include <bit>
#include <cstdint>
uint32_t n = 12u; // 0b1100
// ── Power-of-two check ────────────────────────────────────────────────────
// Classic: works for n > 0; gives wrong answer for n == 0.
bool is_pow2_classic = (n > 0) && ((n & (n - 1u)) == 0u);
// C++20: handles n == 0 correctly (returns false).
bool is_pow2_modern = std::has_single_bit(n); // C++20
// ── Isolate the lowest set bit ────────────────────────────────────────────
// The expression n & (-n) exploits two's-complement: -n flips all bits then
// adds 1, so the result has exactly one bit — the lowest set bit of n.
uint32_t lsb_classic = n & (-n); // 4 (0b0100)
// No direct <bit> equivalent; the trick remains idiomatic.
// ── Clear the lowest set bit ──────────────────────────────────────────────
// Subtracting 1 turns the LSB and all zeros below it into 1s; AND clears them.
uint32_t cleared_classic = n & (n - 1u); // 8 (0b1000) — clears bit 2
// Use case: iterate over all set bits in O(popcount) steps:
uint32_t bits = 0b10110100u;
while (bits) {
int pos = std::countr_zero(bits); // C++20: index of LSB
// process bit at position 'pos' ...
bits &= bits - 1u; // clear it and continue
}
// ── Round up to next power of two ─────────────────────────────────────────
// Classic (32-bit only, breaks for n == 0):
uint32_t next_pow2_classic = n;
--next_pow2_classic;
next_pow2_classic |= next_pow2_classic >> 1;
next_pow2_classic |= next_pow2_classic >> 2;
next_pow2_classic |= next_pow2_classic >> 4;
next_pow2_classic |= next_pow2_classic >> 8;
next_pow2_classic |= next_pow2_classic >> 16;
++next_pow2_classic; // 16
// C++20: one call, handles all cases, UB-safe for in-range values.
uint32_t next_pow2_modern = std::bit_ceil(n); // 16 C++20
// ── Largest power of two <= n ─────────────────────────────────────────────
uint32_t prev_pow2_classic = 1u << (31u - std::countl_zero(n)); // 8
uint32_t prev_pow2_modern = std::bit_floor(n); // 8 C++20
// ── Floor log2 ────────────────────────────────────────────────────────────
int ilog2_classic = 31 - std::countl_zero(n); // 3
int ilog2_modern = static_cast<int>(std::bit_width(n)) - 1; // 3 C++20Endianness (C++20)
#include <bit>
// std::endian: C++20. if constexpr: C++17.
if constexpr (std::endian::native == std::endian::little) {
// x86, ARM (typical LE mode)
} else if constexpr (std::endian::native == std::endian::big) {
// MIPS, SPARC, network byte order
}
// C++23: std::byteswap — single-instruction byte reversal
// C++20 and earlier: manual implementation
constexpr uint32_t bswap32(uint32_t n) noexcept {
return ((n & 0xFF000000u) >> 24u)
| ((n & 0x00FF0000u) >> 8u)
| ((n & 0x0000FF00u) << 8u)
| ((n & 0x000000FFu) << 24u);
}
constexpr uint32_t to_network_order(uint32_t host) noexcept {
if constexpr (std::endian::native == std::endian::little)
return bswap32(host);
return host;
}Best Practices
- Use unsigned types exclusively for bit operations.
<bit>functions reject signed types at compile time. Pre-C++20 compilers have implementation-defined behavior for right-shifting negatives. - Use
1uor1ULL, never bare1.uint64_t mask = 1 << 40is undefined behavior —1is a signed 32-bitint. Write1ULL << 40oruint64_t{1} << 40. - Prefer
<bit>over intrinsics.std::popcountcompiles topopcnton x86 with optimizations; it gains nothing over__builtin_popcountexcept portability andconstexprevaluation. - Use
std::bit_castfor type punning. It is the only well-defined,constexpr-capable mechanism for reinterpreting bit patterns between unrelated types. - Validate bit layouts at compile time. For packed protocol headers,
static_assertthat field widths sum to the expected total — far cheaper than debugging misaligned packets in production. - Prefer
std::bitsetfor named, human-readable flag sets. When the number of bits is small and fixed,std::bitsetwith named enumerators is more maintainable than raw integer masks, and itsto_string()output is invaluable for debugging.
Common Pitfalls
Shift amount out of range. Shifting by a value ≥ the bit-width of the type is undefined behavior in all C++ standards:
uint32_t x = 1u;
x << 32; // UB — amount equals type width
x << -1; // UB — negative shift amountInteger promotion turns ~ on narrow types into int. Bitwise operations on types narrower than int promote to int before the operator fires. ~uint8_t{0xFF} produces int{0xFFFFFF00}, not uint8_t{0x00}. The silent truncation when the result is assigned back to a uint8_t gives the right value, but the intermediate int can cause surprises in wider expressions:
uint8_t a = 0xFFu;
auto b = ~a; // b is int{0xFFFFFF00} — NOT uint8_t{0x00}
uint8_t c = ~a; // c == 0x00: truncation gives the correct final value,
// but relying on implicit narrowing is fragile
uint8_t d = static_cast<uint8_t>(~a); // explicit and correct
// The hazard becomes visible here:
uint16_t mask = ~uint8_t{0x0Fu}; // mask == 0xFFF0, not 0x00F0 as intended
uint16_t safe = static_cast<uint8_t>(~uint8_t{0x0Fu}); // 0x00F0 — correctstd::bit_ceil overflow. If the value exceeds half the type's maximum, the next power of two is unrepresentable — undefined behavior:
std::bit_ceil(uint8_t{200}) // UB: next pow2 is 256, not representable in uint8_tGuard with std::has_single_bit(n) || n <= (std::numeric_limits<T>::max() / 2 + 1) before calling std::bit_ceil on near-max values, or widen to the next type first.
std::bit_cast requires identical sizes. Both types must be the same size and trivially copyable — enforced at compile time:
// Error: sizeof(double) == 8, sizeof(uint32_t) == 4
auto x = std::bit_cast<uint32_t>(3.14); // compile error
// Correct: use uint64_t for double
auto y = std::bit_cast<uint64_t>(3.14); // OKstd::bitset is not zero-overhead. It is convenient for string I/O and flag sets, but its internal layout is not SIMD-friendly and bulk operations are not competitive with hand-vectorized uint64_t arrays. Profile before using in hot paths.
std::byteswap on signed types before C++ standardization. std::byteswap works on all integral types (signed or unsigned) since C++23, but the underlying byte-reversal changes the sign bit's position. Always interpret the result through std::bit_cast or an unsigned view if you need to inspect the bit pattern afterward.