<bit>
C++20 header providing portable bit manipulation — rotation, counting, power-of-two math, endianness detection, and safe type-punning via bit_cast.
<bit>since C++20A standard library header that consolidates portable, well-defined bit manipulation operations over unsigned integer types, together with std::bit_cast for safe type-punning and std::endian for compile-time byte-order detection.
Overview
Before C++20, bit-level work was a minefield. Rotates required non-portable compiler intrinsics (__builtin_rotl, _rotl). Population counts needed __builtin_popcount. Type-punning through unions or raw reinterpret_cast was technically undefined behavior. Endianness detection required #ifdef chains. The <bit> header resolves all of this in one place with a clean, constexpr-friendly API.
Every function that operates on integers restricts its template parameter T to unsigned integer types (unsigned char, unsigned short, unsigned int, unsigned long, unsigned long long, and uint8_t/uint16_t/etc. typedefs). Passing a signed type is ill-formed. bool and char variants are not permitted either.
The header groups into four areas:
- Type-punning —
std::bit_cast - Endianness —
std::endian - Bit counting —
countl_zero,countl_one,countr_zero,countr_one,popcount - Rotation and power-of-two math —
rotl,rotr,has_single_bit,bit_ceil,bit_floor,bit_width
Syntax
#include <bit>
// Type-punning
template<class To, class From>
constexpr To std::bit_cast(const From& from) noexcept; // C++20
// Endianness
enum class std::endian { little, big, native }; // C++20
// Bit counting — all require unsigned T, all constexpr noexcept
template<class T> constexpr int std::countl_zero(T x) noexcept; // leading zeros
template<class T> constexpr int std::countl_one(T x) noexcept; // leading ones
template<class T> constexpr int std::countr_zero(T x) noexcept; // trailing zeros
template<class T> constexpr int std::countr_one(T x) noexcept; // trailing ones
template<class T> constexpr int std::popcount(T x) noexcept; // set bits
// Rotation
template<class T> constexpr T std::rotl(T x, int s) noexcept;
template<class T> constexpr T std::rotr(T x, int s) noexcept;
// Power-of-two
template<class T> constexpr bool std::has_single_bit(T x) noexcept; // x == 2^n
template<class T> constexpr T std::bit_ceil(T x) noexcept; // smallest 2^n >= x
template<class T> constexpr T std::bit_floor(T x) noexcept; // largest 2^n <= x
template<class T> constexpr int std::bit_width(T x) noexcept; // floor(log2(x)) + 1Examples
Type-punning with bit_cast
The canonical use case is extracting the IEEE 754 bit pattern from a float without UB:
#include <bit>
#include <cstdint>
#include <cassert>
float reinterpret_as_float(std::uint32_t bits) {
return std::bit_cast<float>(bits); // C++20: well-defined, constexpr-capable
}
// Inspect the sign bit of a float
bool is_negative(float f) {
auto bits = std::bit_cast<std::uint32_t>(f); // C++20
return (bits >> 31) != 0;
}
// Both types must be trivially copyable and sizeof(To) == sizeof(From)
// std::bit_cast<int>(3.14f); // OK — same size
// std::bit_cast<long>(3.14f); // ill-formed on 64-bit — size mismatchEndianness detection at compile time
#include <bit>
void write_u32_le(std::uint8_t* buf, std::uint32_t val) {
if constexpr (std::endian::native == std::endian::little) { // C++20
// Host is little-endian — memcpy directly
std::memcpy(buf, &val, 4);
} else {
// Byte-swap required
buf[0] = static_cast<std::uint8_t>(val);
buf[1] = static_cast<std::uint8_t>(val >> 8);
buf[2] = static_cast<std::uint8_t>(val >> 16);
buf[3] = static_cast<std::uint8_t>(val >> 24);
}
}std::endian::native equals std::endian::little on x86/x64, std::endian::big on most SPARC/PowerPC targets, and may equal neither on exotic mixed-endian platforms (rare in practice).
Counting bits
#include <bit>
#include <cstdint>
std::uint8_t x = 0b00111010u;
std::countl_zero(x); // 2 — two leading zeros before the first 1
std::countr_zero(x); // 1 — one trailing zero after the last 1
std::popcount(x); // 4 — four bits set
// Practical: find lowest set bit position (like _BitScanForward)
int lowest_set_bit(std::uint32_t mask) {
return std::countr_zero(mask); // C++20, UB-free, constexpr
}
// Count how many flags are active in a bitmask
int active_flags(std::uint32_t flags) {
return std::popcount(flags); // C++20
}Rotation
#include <bit>
// Portable left/right rotate without UB or intrinsics
auto a = std::rotl(std::uint32_t{0x12345678u}, 4); // 0x23456781 — C++20
auto b = std::rotr(std::uint32_t{0x12345678u}, 4); // 0x81234567 — C++20
// Negative shift argument is valid — rotl(x, -n) == rotr(x, n)
auto c = std::rotl(std::uint32_t{0xDEADBEEFu}, -8); // same as rotr(..., 8)Power-of-two math
#include <bit>
#include <cstdint>
// Allocator-style: round up to next power of two
std::size_t next_pow2(std::size_t n) {
return std::bit_ceil(n); // C++20; precondition: result fits in size_t
}
// Check alignment requirement
bool is_power_of_two(std::uint32_t n) {
return n != 0 && std::has_single_bit(n); // C++20
}
// Minimum bits needed to represent a value (floor(log2(n)) + 1)
int bits_needed(std::uint32_t n) {
return std::bit_width(n); // C++20; bit_width(0) == 0
}
// Constexpr table sizing:
constexpr auto TABLE_SIZE = std::bit_ceil(std::size_t{100}); // 128 — C++20Best Practices
Prefer bit_cast over union type-punning or reinterpret_cast. Union-based type-punning is undefined behavior in C++ (only valid in C99+). memcpy works but is not constexpr. bit_cast is the only correct, constexpr-capable option in C++20.
Use unsigned types explicitly. All <bit> algorithms are constrained to unsigned types. Don't silently rely on implicit conversions from int literals — cast or use U suffixes to make the type explicit. Calling std::popcount(-1) is ill-formed (signed type), not a popcount of all bits.
Use has_single_bit instead of (n & (n - 1)) == 0. The classic trick has a subtle bug when n == 0. std::has_single_bit(0) returns false correctly.
bit_ceil has a precondition. If the result would overflow T, behavior is undefined. For uint32_t, bit_ceil(0x80000001u) is UB. Guard with bit_width if the input is untrusted.
All operations are constexpr. This makes them usable in constant expressions for template parameters, array sizes, and if constexpr branches — something compiler intrinsics cannot offer.
Common Pitfalls
Signed integer arguments. Every function taking T constrains T to unsigned types. Passing int or ptrdiff_t is a compile error. Pre-cast signed values before use.
bit_cast size mismatch. sizeof(To) must equal sizeof(From). Trying to cast float (4 bytes) to uint64_t (8 bytes) is ill-formed. The compiler error is immediate but can be confusing in templates.
bit_ceil(0) returns 1. This is specified behavior: the smallest power of two not less than 0 is 1. If your logic expects 0-in/0-out, guard it: n == 0 ? 0 : bit_ceil(n).
bit_width(0) returns 0, not 1. This is intentional — zero requires zero bits — but surprises those expecting bit_width to return at least 1. bit_width(1) returns 1, bit_width(2) returns 2, bit_width(3) returns 2.
endian::native may not equal little or big. On mixed-endian architectures (rare: some DSPs, older ARM configurations), native could differ from both. Code that assumes native == little || native == big should add an else branch with a static_assert(false) or runtime fallback.
Why reinterpret_cast is Undefined Behavior for Type-Punning
std::bit_cast exists to replace a pattern that developers have used for decades but that is technically UB in C++. Understanding exactly why reinterpret_cast is broken explains why bit_cast was necessary.
Three sources of UB in reinterpret_cast-based type-punning
Strict aliasing violation. The C++ compiler is allowed to assume that two pointers of incompatible types never point to the same memory. When you reinterpret_cast<float*>(&my_int) and dereference the result, you break that assumption. The compiler can — and does — generate code that reorders or eliminates reads and writes based on this assumption, producing silent wrong results even at -O1.
// UB: strict aliasing violation — compiler may return 42 even after *b writes 0.0f
int init_vars(int* a, float* b) {
*a = 42;
*b = 0.0f; // compiler assumes a and b don't alias
return *a; // may return 42, not 0 — optimizer is entitled to assume a unchanged
}Alignment violation. reinterpret_cast does not check alignment. A uint16_t must be at an address divisible by 2 on most architectures; casting an unaligned uint8_t* to uint16_t* can produce crashes, wrong values, or silent data corruption depending on the CPU. UBSAN reports this; plain -O2 does not.
Object lifetime. Pre-C++20, reinterpret_cast-ing a buffer to a struct type did not start the object's lifetime, making member access UB. C++20 (P0593R3) partially resolved this for char, unsigned char, and std::byte arrays created by new, but the manual placement-new trick is still the safe pre-C++20 path.
std::bit_cast: the correct solution
std::bit_cast<To>(from) copies the object representation of from into a new object of type To. It is:
- Defined behavior — no aliasing, no alignment, no lifetime issues
constexpr— works in constant expressions, unlikememcpy- Ill-formed at compile time if
sizeof(To) != sizeof(From), or if either type is not trivially copyable — you get an error, not silent UB
#include <bit>
#include <cstdint>
// Safe: extract IEEE 754 bit pattern from a float
uint32_t float_bits(float f) {
return std::bit_cast<uint32_t>(f); // C++20 — constexpr, defined behavior
}
// Decode sign, exponent, and mantissa at compile time
constexpr bool is_negative_zero(float f) {
return std::bit_cast<uint32_t>(f) == 0x8000'0000u;
}
// Works on structs too — both must be trivially copyable and same size
struct Bytes4 { unsigned char b[4]; };
Bytes4 as_bytes(float f) {
return std::bit_cast<Bytes4>(f); // copies all 4 bytes, no UB
}Comparison with alternatives:
| Technique | Defined behavior | constexpr | Size-checked |
|---|---|---|---|
reinterpret_cast | No (strict aliasing UB) | No | No |
| union type-pun | C99 yes / C++ no | No | No |
std::memcpy | Yes | No (until C++23) | Manual |
std::bit_cast (C++20) | Yes | Yes | Yes (compile-time) |
If you need pre-C++20 support, memcpy into a local variable of the destination type is the correct idiom — compilers optimize it to a register move in almost all cases.
Rotation in Depth: rotl and rotr
Left and right rotation ("circular shift") move bits toward the high or low end and wrap the displaced bits around to the other side. No bits are lost — unlike << and >>, which shift bits off the edge.
Formally for rotr(x, s) with N = numeric_limits<T>::digits and r = s % N:
r == 0: returnsxunchangedr > 0:(x >> r) | (x << (N - r))r < 0: equivalent torotl(x, -r)
Negative shift values are valid and cross-rotate, so rotl(x, -3) is the same as rotr(x, 3).
#include <bit>
#include <cstdint>
#include <bitset>
// 8-bit rotation example (matches cppreference output)
// x = 0b00011101
// rotr(x, 1) = 0b10001110 — LSB wraps to MSB
// rotl(x, 1) = 0b00111010 — MSB wraps to LSB
constexpr auto x = std::uint8_t{0b00011101u};
static_assert(std::rotr(x, 1) == 0b10001110u);
static_assert(std::rotl(x, 1) == 0b00111010u);
static_assert(std::rotr(x, 9) == std::rotr(x, 1)); // 9 % 8 == 1
// 32-bit: used in hash functions and cryptography
std::uint32_t hash_mix(std::uint32_t h) {
return std::rotl(h, 13) ^ (h * 0x517CC1B727220A95u);
}Compiler code generation
Modern compilers (GCC, Clang, MSVC) recognize the std::rotl/std::rotr pattern and emit a single ROL/ROR instruction on x86-64. Before C++20, writing a correct, UB-free rotate in portable C++ required a specific idiom that compilers would sometimes recognize:
// Pre-C++20 UB-free rotate (requires correct masking to avoid UB for s==0 or s==N)
uint32_t rotl32_pre20(uint32_t x, int s) {
const auto n = s & 31;
return n == 0 ? x : (x << n) | (x >> (32 - n));
}
// C++20: just use std::rotl<uint32_t>(x, s) — always correct, always one instructionThe <bit> version handles all edge cases (zero shift, shifts larger than bit-width, negative shifts) correctly by specification, without extra masking code in the caller.
Practical uses of rotation:
- Cryptography — SHA-256, SHA-512, ChaCha20, and most stream ciphers use bitwise rotation as a core diffusion step
- Hash functions — FNV, Murmur, xxHash all use rotations to spread bit patterns
- CRC computation — some table-less CRC implementations use rotation
- SIMD lane permutation — ARM NEON and x86 AVX expose rotation-like operations for vectorized bit manipulation
Bit Counting and Hardware Intrinsics
The five counting functions in <bit> map directly to hardware instructions on every mainstream CPU:
| C++20 function | x86-64 instruction | ARM instruction | Meaning |
|---|---|---|---|
std::popcount(x) | POPCNT | VCNT / CNT | Count of 1-bits |
std::countl_zero(x) | LZCNT (or BSR+adjust) | CLZ | Leading zeros from MSB |
std::countr_zero(x) | TZCNT (or BSF) | CTZ (via RBIT+CLZ) | Trailing zeros from LSB |
std::countl_one(x) | ~x + LZCNT | ~x + CLZ | Leading ones from MSB |
std::countr_one(x) | ~x + TZCNT | ~x + CTZ | Trailing ones from LSB |
Note on LZCNT vs BSR: on x86, BSR (Bit Scan Reverse) is available without a feature flag but returns N-1-clz(x) for non-zero inputs and is undefined for zero. LZCNT (from BMI1 / ABM) directly gives the leading-zero count and returns N for zero input, which is what the standard requires. Compilers emit LZCNT when targeting CPUs that support it, and fall back to BSR-based code otherwise.
Practical patterns using bit counting
#include <bit>
#include <cstdint>
// --- popcount patterns ---
// Test whether an integer has exactly one bit set (power-of-two check)
// Equivalent to has_single_bit, but works mid-expression
bool is_power_of_two(uint32_t n) {
return n != 0 && std::popcount(n) == 1;
}
// Count active CPU features in a bitmask
int active_feature_count(uint32_t feature_flags) {
return std::popcount(feature_flags); // compiles to POPCNT on x86
}
// --- countl_zero / bit_width patterns ---
// Integer log2 (floor) — bit_width(x) - 1 for x > 0
int ilog2(uint32_t x) {
return std::bit_width(x) - 1; // bit_width(0)==0, so guard if x can be 0
}
// Round up to next power of two (same as bit_ceil, shown step-by-step)
uint32_t next_pow2(uint32_t n) {
if (n <= 1) return 1;
return uint32_t{1} << std::bit_width(n - 1); // == bit_ceil(n)
}
// --- countr_zero patterns ---
// Find the position of the lowest set bit (like _BitScanForward / __builtin_ctz)
int lowest_set_bit_pos(uint32_t mask) {
return std::countr_zero(mask); // UB-free, constexpr; mask==0 returns 32
}
// Iterate over all set bits (common in bitmask/graph algorithms)
void for_each_set_bit(uint32_t mask, auto callback) {
while (mask != 0) {
int bit = std::countr_zero(mask); // position of lowest set bit
callback(bit);
mask &= mask - 1; // clear lowest set bit
}
}
// --- countl_one pattern ---
// Count the length of a contiguous block of 1-bits from the MSB
// Used in subnet mask validation (e.g., 0xFFFF0000 has 16 leading ones)
int prefix_length(uint32_t subnet_mask) {
return std::countl_one(subnet_mask);
}popcount in the context of vectorization
When std::popcount is applied inside a loop over a vector of integers, GCC and Clang with -O2 -mavx2 will often auto-vectorize using VPSHUFB-based lookup tables (since AVX2 has no direct VPOPCNT until AVX-512 VPOPCNTDQ). The standard function makes this optimization legal; compiler-intrinsic versions can block it if the compiler cannot prove the argument types.
Endian-Aware Serialization
std::endian (C++20)
std::endian is a scoped enum (not a function) that lets you detect host byte order at compile time:
#include <bit>
// Three possible values:
// std::endian::little — least-significant byte at lowest address (x86, ARM)
// std::endian::big — most-significant byte at lowest address (SPARC, some PowerPC)
// std::endian::native — equals little or big on conforming platforms
// may differ on mixed-endian targets (rare)
static_assert(std::endian::native == std::endian::little
|| std::endian::native == std::endian::big,
"Mixed-endian architecture — implement platform-specific fallback");std::byteswap (C++23)
std::byteswap<T>(n) reverses all bytes of the integral value n. It is constexpr noexcept and is ill-formed if T has padding bits. On x86-64, it compiles to a single BSWAP or MOVBE instruction.
#include <bit>
#include <cstdint>
// Simple reversal
constexpr auto x = std::uint32_t{0xDEAD'BEEFu};
static_assert(std::byteswap(x) == 0xEFBE'ADDEu); // C++23
// uint16_t: 0xCAFE -> 0xFECA
// uint64_t: 0x0123456789ABCDEF -> 0xEFCDAB8967452301Endian-aware serialization pattern
Combining std::endian and std::byteswap gives a clean, portable serialization helper:
#include <bit>
#include <cstdint>
#include <array>
#include <concepts>
// Convert any integer to big-endian byte order (network byte order)
// C++23 version using std::byteswap
template<std::integral T>
T to_big_endian(T value) {
if constexpr (std::endian::native == std::endian::big)
return value; // already big-endian — no-op
else
return std::byteswap(value); // C++23
}
// Serialize a uint32_t as 4 big-endian bytes into a buffer
void write_u32_be(std::uint8_t* buf, std::uint32_t val) {
const auto be = to_big_endian(val);
// bit_cast to byte array — safe, constexpr-capable (C++20)
const auto bytes = std::bit_cast<std::array<std::uint8_t, 4>>(be);
buf[0] = bytes[0];
buf[1] = bytes[1];
buf[2] = bytes[2];
buf[3] = bytes[3];
}
// C++20 fallback (no byteswap): manual byte extraction
std::uint32_t to_big_endian_cpp20(std::uint32_t value) {
if constexpr (std::endian::native == std::endian::big)
return value;
return ((value & 0xFF00'0000u) >> 24)
| ((value & 0x00FF'0000u) >> 8)
| ((value & 0x0000'FF00u) << 8)
| ((value & 0x0000'00FFu) << 24);
}Platform support table
| Platform | std::endian::native | BSWAP equivalent |
|---|---|---|
| x86 / x86-64 | little | BSWAP r32/r64, MOVBE (Atom/Haswell+) |
| ARM (little-endian mode, default) | little | REV, REV16, REV32 |
ARM (big-endian mode, armeb) | big | REV (swap to little) |
| SPARC, PowerPC (traditional) | big | lwbrx / target-specific |
| RISC-V | little | rev8 (Zbb extension) |
The practical implication: on x86, to_big_endian(val) with C++23 compiles to exactly one BSWAP instruction when the value is already in a register. The if constexpr branch that returns unchanged on big-endian targets means zero overhead there too.
What about htonl/ntohl?
The POSIX functions htonl, htons, ntohl, ntohs (<arpa/inet.h>) are the traditional way to convert between host and network (big-endian) byte order. They work, but:
- They are not
constexpr - They require a POSIX header (not available on Windows without Winsock)
- They only handle 16-bit and 32-bit types
std::endian + std::byteswap is the portable, constexpr-capable, header-only C++ replacement.
See Also
<bitset>— fixed-size bit arrays with named bit operations at runtimestd::atomic— C++20 addedfetch_or,fetch_and,fetch_xorandstd::atomic_reffor atomic bit manipulation