std::bitset
Fixed-size bit manipulation with set/reset/flip/test, bitwise operators, and integer/string conversions — stack-allocated, no heap, no UB from shift overflow.
std::bitset<N>since C++98A fixed-size, stack-allocated sequence of N bits with named operations (set, reset, flip, test), full bitwise operators, and conversions to integers and strings — all without heap allocation.
Overview
std::bitset<N> packs N bits into one or more machine words. The size is a compile-time constant: you cannot grow or shrink it at runtime. In return you get zero heap allocation, no UB from shifting past a word boundary, and a named API that replaces manual bit twiddling.
Bits are indexed 0 = least significant — bit 0 is the rightmost character when printed. This is the single most common source of confusion: bitset<8>(1).to_string() is "00000001", not "10000000".
Since C++23, the majority of bitset member functions are constexpr.
When to prefer bitset over alternatives:
bitset<N> | uint64_t | vector<bool> | boost::dynamic_bitset | |
|---|---|---|---|---|
| Size | Compile-time | 64 bits | Runtime | Runtime |
| Storage | Stack | Register | Heap | Heap |
| Named ops | Yes | Manual | Limited | Yes |
count() (popcount) | Yes | std::popcount (C++20) | Yes (slow) | Yes |
| Iterate set bits | Workaround needed | Manual loop | Range-for (slow) | find_first()/find_next() |
| Max N | Unlimited | 64 | Unlimited | Unlimited |
Prefer bitset over a uint64_t bitmask when N > 64 or when named operations make intent clearer. Prefer it over vector<bool> when N is fixed at compile time. When N is only known at runtime, reach for boost::dynamic_bitset or a vector<uint64_t> with manual masking.
Syntax
Construction
#include <bitset>
std::bitset<8> a; // all-zero (default)
std::bitset<8> b(0b10110100); // from binary literal (C++14)
std::bitset<8> c(0xB4); // equivalent hex literal
std::bitset<4> d(255); // only low N bits used → 1111
// String constructor — throws std::invalid_argument on any non-zero/one char
std::bitset<8> e("10110100"); // direct string literal
std::bitset<8> f(std::string("10110100"));
// Substring form: (string, pos, count)
std::string s = "XX1010XX";
std::bitset<4> g(s, 2, 4); // reads chars [2..5] → "1010"
// Custom zero/one characters
std::bitset<4> h("abba", 4, 'a', 'b'); // 'a'→0, 'b'→1 → 0110Access and queries
std::bitset<8> b(0b10110101);
bool v1 = b[3]; // no bounds check — UB on write if pos >= N
bool v2 = b.test(3); // throws std::out_of_range if pos >= N
b.count(); // popcount: number of set bits
b.size(); // N — constexpr since C++23
b.any(); // true if at least one bit is set
b.all(); // true if all bits are set
b.none(); // true if no bits are setModification
std::bitset<8> b;
b.set(); // all bits → 1
b.reset(); // all bits → 0
b.flip(); // invert all bits
b.set(3); // bit 3 → 1
b.reset(3); // bit 3 → 0
b.flip(3); // bit 3 toggled
b.set(5, false); // set(pos, value) — bit 5 → 0Bitwise operators
All standard operators work between two bitset<N> values of the same N:
std::bitset<8> a(0b11001100), b(0b10101010);
auto c = a & b; // AND → 10001000
auto d = a | b; // OR → 11101110
auto e = a ^ b; // XOR → 01100110
auto f = ~a; // NOT → 00110011
auto g = a << 2; // left shift → 00110000
auto h = a >> 2; // right shift → 00110011
a &= b; a |= b; a ^= b; a <<= 1; a >>= 1; // compound formsConversion
std::bitset<16> b(0xDEAD);
unsigned long ul = b.to_ulong(); // throws std::overflow_error if high bits set and N > 32
unsigned long long ull = b.to_ullong(); // C++11 — safe for N <= 64
std::string s = b.to_string(); // "1101111010101101"
std::string sx = b.to_string('.', 'X'); // custom zero/one charsExamples
Permission flags with a scoped enum
enum class Perm : std::size_t { Read = 0, Write = 1, Exec = 2, Admin = 3 };
auto idx = [](Perm p) { return static_cast<std::size_t>(p); };
std::bitset<4> perms;
perms.set(idx(Perm::Read));
perms.set(idx(Perm::Write));
if (perms.test(idx(Perm::Write))) std::puts("can write");
if (!perms.test(idx(Perm::Exec))) std::puts("cannot execute");
perms.reset(idx(Perm::Write)); // revoke write
perms.flip(idx(Perm::Admin)); // toggle adminSieve of Eratosthenes
bitset is the canonical container for a memory-efficient sieve. One bit per candidate costs ~125 KB for 1 million numbers, versus ~1 MB for vector<bool> (heap-allocated) or ~4 MB for vector<int>.
#include <bitset>
#include <iostream>
constexpr int LIMIT = 1'000'000;
static std::bitset<LIMIT + 1> is_prime; // static: ~125 KB — too large for a stack frame
void sieve() {
is_prime.set();
is_prime.reset(0);
is_prime.reset(1);
for (int i = 2; i * i <= LIMIT; ++i) {
if (is_prime[i]) {
for (int j = i * i; j <= LIMIT; j += i)
is_prime.reset(j);
}
}
}
int main() {
sieve();
std::cout << is_prime.count() << '\n'; // 78498
}Iterating set bits
bitset has no iterator. Three approaches, in order of portability:
std::bitset<64> b(0b10110101001100ULL);
// Approach 1: simple loop — always portable, O(N)
for (std::size_t i = 0; i < b.size(); ++i)
if (b[i]) process(i);
// Approach 2: portable for N <= 64 — O(k) where k is the number of set bits
unsigned long long val = b.to_ullong();
while (val) {
int pos = __builtin_ctzll(val); // GCC/Clang; MSVC: _BitScanForward64
process(pos);
val &= val - 1; // clear lowest set bit
}
// Approach 3: libstdc++ extension — fast but non-standard, do not use in portable code
for (std::size_t i = b._Find_first(); i < b.size(); i = b._Find_next(i))
process(i);Best Practices
Use test() at trust boundaries. operator[] skips bounds checking even in debug builds. Prefer test() when the index comes from external input; use operator[] only in performance-critical inner loops with a provably valid index.
Declare large bitsets at namespace scope or static. The stack is typically 1–8 MB. bitset<8'000'000> is 1 MB as a local variable and will overflow the stack silently on most platforms. Namespace scope or static local storage is the right home for any bitset over ~64 KB.
Prefer to_ullong() over to_ulong(). On Windows, unsigned long is 32 bits; to_ulong() throws std::overflow_error for N > 32 with any high bit set. to_ullong() (C++11) handles up to N = 64 safely.
Round N to a multiple of 64 when practical. Compilers implement bitset as an array of unsigned long (typically 64-bit). bitset<65> occupies two words and pays for partial-word masking on every operation; bitset<64> occupies one. If you have flexibility in N, favor multiples of 64.
Common Pitfalls
auto x = b[i] captures a proxy, not bool. operator[] returns std::bitset::reference, a proxy object that holds a pointer into the bitset. auto val = b[i] gives you that proxy. If the bitset is then modified or destroyed, the proxy reflects the new state or dangles. Always write bool val = b[i] or auto val = static_cast<bool>(b[i]).
Bit 0 is rightmost, not leftmost. bitset<8>(1) prints as "00000001". When initializing from an integer or indexing by position, remember that index 0 corresponds to the least-significant bit and the rightmost print character.
String constructor throws on unexpected characters. The string constructor throws std::invalid_argument if it encounters any character that is not the designated zero or one character. A space, a newline, or a digit other than 0/1 will throw. Sanitize before constructing from user input.
to_ulong() / to_ullong() throw on overflow at runtime. There is no compile-time protection. A bitset<128> with bit 64 set will throw std::overflow_error from to_ullong() — at runtime, potentially in production. Guard with a size check or mask the upper portion before converting.
See Also
std::vector<bool>— dynamically sized bit sequence; heap-allocated, slower element access, no bitwise operators across the whole setstd::popcount— C++20 hardware popcount for unsigned integer types, from<bit>- Bit manipulation utilities —
<bit>— C++20:std::countl_zero,std::countr_zero,std::rotl,std::rotr,std::has_single_bit boost::dynamic_bitset— runtime-sized bitset withfind_first()/find_next()for efficient set-bit iteration