Skip to content
C++
Library
since C++98
Basic

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++98

A 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_tvector<bool>boost::dynamic_bitset
SizeCompile-time64 bitsRuntimeRuntime
StorageStackRegisterHeapHeap
Named opsYesManualLimitedYes
count() (popcount)Yesstd::popcount (C++20)Yes (slow)Yes
Iterate set bitsWorkaround neededManual loopRange-for (slow)find_first()/find_next()
Max NUnlimited64UnlimitedUnlimited

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

cpp
#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 → 0110

Access and queries

cpp
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 set

Modification

cpp
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 → 0

Bitwise operators

All standard operators work between two bitset<N> values of the same N:

cpp
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 forms

Conversion

cpp
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 chars

Examples

Permission flags with a scoped enum

cpp
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 admin

Sieve 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>.

cpp
#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:

cpp
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 set
  • std::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 with find_first()/find_next() for efficient set-bit iteration