Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 10 – Standard containers

std::bitset and the std::vector<bool> trap

Store flags in std::bitset, speed up set computations with whole-word bit operations, and see why std::vector<bool> hands out proxies instead of bool&.

  • Intermediate
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Use std::bitset for fixed-size sets of flags and read its bit order correctly
  • Optimise set computations with whole-word shifts and ors on a bitset
  • Recognise the proxy-reference problems of std::vector<bool> and choose a container without them

Before you start

A hundred million yes-or-no flags take 12.5 MB as bits and 100 MB as bytes, and with bits a single shift and or can update 64 of them at once. std::bitset<N> is a fixed-size array of N bits with the bitwise operators built in: set, clear and test one bit, count the 1s, and combine two bitsets with &, |, ^, ~, << and >>, each working on a whole machine word at a time. N is part of the type, fixed when the program is compiled.

std::vector<bool> is the run-time-sized alternative, and it is a trap. It packs its bits too, and because a bit has no address of its own, its operator[] returns a small proxy object: An object returned in place of a reference, standing for an element that cannot be referred to directly, as std::vector<bool>::reference stands for one bit. It points into its container and is valid only while the container lives. More in “C++ auto, decltype and type aliases” instead of a bool&: bool& r = v[0]; does not compile, auto b = v[0]; keeps a link to the vector, and two threads that write different elements can race. You will step through both on Apple clang 21 and GCC 16, time the whole-word operations, and pick a container for each kind of flag.

A bitset<12> holds 12 seats, and its text puts seat 0 last

One row of 12 cinema seats makes a good bitset: bit i is seat i, and a 1 means the seat is booked.

seats.cpp C++ · seats.cpp
// One row of 12 cinema seats as a std::bitset<12>: bit i is seat i, and 1 means booked.
#include <bit>
#include <bitset>
#include <print>
#include <stdexcept>

int main() {
    std::bitset<12> booked;          // all 12 bits start at 0
    booked.set(0);
    booked.set(3);
    booked.set(4);
    booked[7] = true;                // operator[] returns a proxy you can assign through
    std::println("booked:  {}  {} of {} seats", booked.to_string(), booked.count(), booked.size());

    const std::bitset<12> aisle("100000000001");   // the text runs from seat 11 down to seat 0
    const std::bitset<12> free_seats = ~booked;
    std::println("aisle:   {}  seat 0 is on the aisle: {}", aisle.to_string(), aisle.test(0));
    std::println("free:    {}", free_seats.to_string());
    std::println("free aisle seats: {}", (free_seats & aisle).count());
    std::println("first free seat: {}", std::countr_zero(free_seats.to_ullong()));   // only for N <= 64

    booked |= std::bitset<12>(0b0000'0110'0000);   // book seats 5 and 6 in one operation
    std::println("booked:  {}  any free: {}", booked.to_string(), !booked.all());

    try {
        [[maybe_unused]] bool taken = booked.test(12);   // test() checks the position and throws
    } catch (const std::out_of_range&) {
        std::println("test(12) threw std::out_of_range");
    }
}

Its session compiles the program with Apple clang and with GCC and then runs each build; lines after a $ are commands, the rest is their output. The track’s first lesson explains how to repeat a session on your own computer:

seats.cpp, built and run with both compilers Python · seats_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra seats.cpp -o seats && ./seats
g++-16 -std=c++23 -Wall -Wextra seats.cpp -o seats && ./seats
""")

Output

$ clang++ -std=c++23 -Wall -Wextra seats.cpp -o seats && ./seats
booked:  000010011001  4 of 12 seats
aisle:   100000000001  seat 0 is on the aisle: true
free:    111101100110
free aisle seats: 1
first free seat: 1
booked:  000011111001  any free: true
test(12) threw std::out_of_range
$ g++-16 -std=c++23 -Wall -Wextra seats.cpp -o seats && ./seats
booked:  000010011001  4 of 12 seats
aisle:   100000000001  seat 0 is on the aisle: true
free:    111101100110
free aisle seats: 1
first free seat: 1
booked:  000011111001  any free: true
test(12) threw std::out_of_range

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 seats_session.py

The members do what their names say: set, reset and flip change one bit or, without an argument, all of them; test reads one; count counts the 1 bits; all, any and none answer the obvious questions about the whole set; size returns N. Step through the program’s operations on the seats, each bit string as to_string() prints it:

Interactive · step by step Twelve seats in a std::bitset<12> Step through the program's operations. Each box shows a bitset as to_string() prints it, seat 11 first and seat 0 last.
Twelve seats in a std::bitset<12>, step 8 of 8. Showing booked (000010011001), aisle("100000000001"), free = ~booked, free & aisle, first free seat, booked |= seats 5, 6. In focus: booked.~ flips all 12&&to_ullong()booked000010011001aisle("100000000001")seats 0 and 11free = ~booked111101100110free & aisle100000000000: 1 seatfirst free seatcountr_zero gives 1booked |= seats 5, 6000011111001test(12): std::out_of_range

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Twelve free seats. std::bitset<12> booked starts with all 12 bits at 0. Bit i is seat i, and to_string() writes bit 11 first and bit 0 last, so seat 0 is the rightmost character.
  2. Four seats booked. set(0), set(3) and set(4) book three seats, and booked[7] = true books a fourth through the proxy that operator[] returns. The 1s appear from the right: 000010011001, and count() is 4.
  3. Seats from a text. The aisle seats come from the text "100000000001". Its last character is bit 0, so seats 0 and 11 are set, and test(0) is true.
  4. ~ flips every bit. free_seats = ~booked turns every 0 into 1 and every 1 into 0: 111101100110, the eight seats nobody has booked.
  5. & keeps what both have. free_seats & aisle keeps the bits set in both: 100000000000. count() is 1: seat 11 is a free aisle seat, while seat 0 is booked.
  6. The first free seat. A bitset has no member that finds its lowest 1. For 12 bits, to_ullong() turns it into an integer, and std::countr_zero counts the 0s below its lowest 1: seat 1.
  7. Two seats in one operation. booked |= std::bitset<12>(0b0000'0110'0000) books seats 5 and 6 at once: 000011111001. !booked.all() is still true, because six seats are left.
  8. test() checks the position. booked.test(12) asks for a bit the set does not have, so it throws std::out_of_range, as the program printed. booked[12] is not checked: it is undefined behaviour.

Four details explain the rest of the output:

  • The text runs backwards. to_string() writes bit N - 1 first and bit 0 last, the way numbers are written, so seat 0 is the rightmost character. The string constructor reads the same way: in "100000000001" the last character is bit 0. The standard says so in the constructor’s wording: the last character of the string corresponds to bit position zero.
  • test checks, [] does not. test(12) on a 12-bit set throws std::out_of_range. operator[] has a precondition instead: an index outside the set is undefined behaviour, and in C++26 a hardened precondition that a hardened library checks (P3471R4).
  • Whole-set operations need the same N. free_seats & aisle combines two std::bitset<12>; a bitset<12> and a bitset<16> do not mix.
  • There is no “find the first set bit”. For N up to 64, convert with to_ullong() and use std::countr_zero from <bit>, as the program does with the free seats. libstdc++ offers _Find_first() and _Find_next(), but they are extensions of that library, so code that uses them does not build with libc++.

operator[] on a non-const bitset returns std::bitset<N>::reference, a proxy object that knows its bitset and the bit’s position, which is how booked[7] = true can change a single bit. Keep that word, proxy, in mind: it is the whole story of std::vector<bool> below.

Reading the bit string from the left

In std::bitset<6> b("000101"), the set bits are 0 and 2, not 3 and 5: the last character is bit 0. When you build a bitset from text, write the text the way to_string() prints it, highest bit first.

Checkpoint

Check: Bit order, test() and []

  1. Question 1 of 2 booked.to_string() printed 000010011001. Which character of that text is seat 0?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The last one, on the right

    The text reads like a binary number: bit 11 on the left and bit 0 on the right. So 000010011001 has seats 0, 3, 4 and 7 booked.

  2. Question 2 of 2 On a std::bitset<12>, what do booked.test(12) and booked[12] do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: test(12) throws std::out_of_range; booked[12] is undefined behaviour

    test(pos) throws out_of_range when pos is not less than N. operator[] has the precondition pos < N, so breaking it is undefined behaviour, which C++26 makes a hardened precondition.

Programmer Calculator (Bitwise) Type a value and watch every bit flip as you apply &, |, ^, ~ and shifts at 8 to 128 bits.

A flag costs a bit in a bitset or a vector<bool>, and a byte in a vector<char>

A bitset stores its bits in whole machine words, and std::vector<bool> packs its bits the same way, while a std::vector<char> spends a byte on every flag:

memory.cpp C++ · memory.cpp
// The memory behind a hundred million flags, and the size of a few bitsets.
#include <bitset>
#include <cstddef>
#include <print>
#include <vector>

int main() {
    constexpr std::size_t n = 100'000'000;
    const std::vector<bool> packed(n);   // capacity() counts bits
    const std::vector<char> bytes(n);    // capacity() counts chars, one byte each
    std::println("vector<bool>: {:>11} bytes for {} flags", packed.capacity() / 8, packed.size());
    std::println("vector<char>: {:>11} bytes for {} flags", bytes.capacity(), bytes.size());
    std::println("bitset<n>:    {:>11} bytes, wherever it lives", sizeof(std::bitset<n>));
    std::println("sizeof bitset<1>: {}, <64>: {}, <65>: {}, <1000>: {}", sizeof(std::bitset<1>), sizeof(std::bitset<64>),
                 sizeof(std::bitset<65>), sizeof(std::bitset<1000>));
}
A hundred million flags, three ways Python · memory_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra memory.cpp -o memory && ./memory
g++-16 -std=c++23 -Wall -Wextra memory.cpp -o memory && ./memory
# The stack limit of this shell, in kilobytes: a local bitset<100000000> would not fit
ulimit -s
""")

Output

$ clang++ -std=c++23 -Wall -Wextra memory.cpp -o memory && ./memory
vector<bool>:    12500000 bytes for 100000000 flags
vector<char>:   100000000 bytes for 100000000 flags
bitset<n>:       12500000 bytes, wherever it lives
sizeof bitset<1>: 8, <64>: 8, <65>: 16, <1000>: 128
$ g++-16 -std=c++23 -Wall -Wextra memory.cpp -o memory && ./memory
vector<bool>:    12500000 bytes for 100000000 flags
vector<char>:   100000000 bytes for 100000000 flags
bitset<n>:       12500000 bytes, wherever it lives
sizeof bitset<1>: 8, <64>: 8, <65>: 16, <1000>: 128
# The stack limit of this shell, in kilobytes: a local bitset<100000000> would not fit
$ ulimit -s
8176

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 memory_session.py

A hundred million flags take 12.5 MB as bits and 100 MB as bytes, eight times more, on both libraries. The sizes of the small bitsets show the other side: storage is a whole number of 64-bit words, so even std::bitset<1> takes 8 bytes, std::bitset<65> takes 16 and std::bitset<1000> takes 128. A bitset lives wherever you declare it, and a std::bitset<100000000> declared as a local variable needs 12.5 MB of stack, more than the 8,176 KB (about 8 MB) the shell’s ulimit -s allowed here, so the program would crash. Make large bitsets static, a global, or put them on the heap with std::make_unique<std::bitset<N>>(), as the exercise at the end does.

Checkpoint

Check: Memory per flag

  1. Question 1 of 2 How many bytes did the std::vector<bool> of 100,000,000 flags take for its bits on both libraries?

    Type a number.

    A hint

    Eight flags fit in a byte.

    Show the answer: Question 1 of 2

    Answer: 12500000 bytes

    One bit per flag: 100,000,000 / 8 = 12,500,000 bytes, as memory_session.py printed. A vector<char> took 100,000,000 bytes, eight times more.

  2. Question 2 of 2 Why would std::bitset<100000000> seen; as a local variable of main crash on the lesson's Mac?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It needs 12.5 MB of stack, and the stack allowed 8,176 KB

    A bitset is a plain object with no heap memory, so a local one takes its whole size from the stack. Make it static, a global, or allocate it with std::make_unique.

One shift and one or update 64 totals at a time

The bitset operators work on whole words, so one instruction updates 64 bits: this is bit parallelism: Working on many flags at once by packing them into machine words, so that one bitwise instruction, such as a shift or an or, updates 64 of them. reach |= reach << w updates every total of a subset-sum problem this way.. Problems that update a large set of flags in the same way at once can use it directly. A classic one is subset sum: given some weights, which totals can a subset of them make? Keep a bit for every total, starting with only total 0 set (the empty subset). For each weight w, every total t that is reachable makes t + w reachable too, and reach << w moves every reachable total w places up at once, so one line updates every total:

Rows of bits for the totals 0 to 8, showing how reach |= reach << w adds the weights 3 and 5 to every reachable total.total012345678startreach100000000weight 3reach << 3000100000reach |= …100100000weight 5reach << 5000001001reach |= …100101001

Reachable totals of the weights 3 and 5, one shift and one or per weight

Text description of the diagram

Each row shows nine bits, one for each total from 0 to 8; a 1 means the total can be made from the weights used so far.

  • At the start only total 0 is reachable (the empty choice), so only bit 0 is 1.
  • For the weight 3, reach << 3 moves every 1 three totals up: bit 3. Or-ing it into reach gives the totals 0 and 3.
  • For the weight 5, reach << 5 moves both 1s five totals up: bits 5 and 8. Or-ing gives the totals 0, 3, 5 and 8.

Every reachable total gains the new weight in the same operation, so one shift and one or update all the totals at once, 64 of them per machine word.

Step through the same two weights, 3 and 5, one operation at a time. The bits are written as to_string() writes them, total 8 first and total 0 last:

Interactive · step by step Subset sums for the weights 3 and 5, one shift and one or at a time Step through the updates. Each box shows the bits for the totals 8 down to 0; a 1 means the total can be made.
Subset sums for the weights 3 and 5, one shift and one or at a time, step 6 of 6. Showing reach at the start, reach << 3, after the weight 3, reach << 5, after the weight 5.<< 3|=kept<< 5|=keptreach at the start000000001: total 0reach << 3000001000after the weight 3000001001: 0 and 3reach << 5100100000after the weight 5100101001: 0, 3, 5, 8

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Only total 0. Nine bits stand for the totals 0 to 8, total 8 first. At the start only total 0 can be made, by choosing no weight at all, so only the last bit is 1.
  2. Shift by 3. reach << 3 moves every 1 three places up: the 1 of total 0 lands on total 3. Each total t that can be made says that t + 3 can be made too, with the weight 3 added.
  3. Or it in. reach |= reach << 3 keeps every total it had and adds the new ones: 000001001, the totals 0 and 3. One shift and one or updated every total.
  4. Shift by 5. For the weight 5 the shift moves both 1s five places up at once: the totals 0 and 3 become 5 and 8.
  5. Or it in again. The or gives 100101001: the totals 0, 3, 5 and 8 can be made, and 1, 2, 4, 6 and 7 cannot. Each weight is used at most once, because every shift starts from the totals made without it.
  6. At full size. The program does the same with a std::bitset<100001> and 1,000 weights. Each shift and each or moves 64 totals per machine word: 1,563 words per weight, about 1.6 million word shifts in all.

The program below answers the question for 1,000 weights between 1 and 150 and totals up to 100,000, once with a plain loop over std::vector<bool>, once with the same loop over std::vector<char>, and once with reach |= reach << w on a std::bitset<100001>. Each version runs five times, taking turns with the other two, and its fastest time is printed, so that other programs on the computer disturb the result less:

subset_sum.cpp C++ · subset_sum.cpp
// Which totals can a subset of 1,000 weights reach? The same question answered three ways, timed: each way runs
// 5 times, taking turns with the others, and the fastest of its times is printed.
#include <bitset>
#include <chrono>
#include <cstddef>
#include <print>
#include <random>
#include <vector>

constexpr std::size_t max_total = 100'000;

template <class Flags>   // std::vector<bool> or std::vector<char>: one flag per total, updated one total at a time
std::size_t count_with_loop(const std::vector<int>& weights) {
    Flags reach(max_total + 1);
    reach[0] = true;
    for (int w : weights) {
        for (std::size_t t = max_total; t >= static_cast<std::size_t>(w); --t) {   // high to low: each weight used once
            if (reach[t - w]) reach[t] = true;
        }
    }
    std::size_t n = 0;
    for (std::size_t t = 0; t <= max_total; ++t) n += reach[t] ? 1 : 0;
    return n;
}

std::size_t count_with_bitset(const std::vector<int>& weights) {
    std::bitset<max_total + 1> reach;
    reach[0] = true;
    for (int w : weights) reach |= reach << w;   // every reachable total t also makes t + w reachable
    return reach.count();
}

struct Way {
    const char* name;
    std::size_t (*count)(const std::vector<int>&);
    double best_ms = 1e300;
    std::size_t reachable = 0;
};

int main() {
    std::mt19937 gen(42);
    std::vector<int> weights(1'000);
    for (int& w : weights) w = 1 + static_cast<int>(gen() % 150);   // weights 1 to 150

    Way ways[] = {{"vector<bool> loop", count_with_loop<std::vector<bool>>},
                  {"vector<char> loop", count_with_loop<std::vector<char>>},
                  {"bitset shifts", count_with_bitset}};
    for (int round = 0; round < 5; ++round) {   // the three take turns, so a busy moment hurts them all alike
        for (Way& w : ways) {
            const auto start = std::chrono::steady_clock::now();
            w.reachable = w.count(weights);
            const std::chrono::duration<double, std::milli> ms = std::chrono::steady_clock::now() - start;
            if (ms.count() < w.best_ms) w.best_ms = ms.count();
        }
    }
    for (const Way& w : ways) std::println("{:<18} {:>6} totals reachable {:9.2f} ms", w.name, w.reachable, w.best_ms);
}
Three ways to find every reachable total, timed Python · subset_sum_session.py
from session import run

run("""
clang++ -std=c++23 -O2 -Wall -Wextra subset_sum.cpp -o subset_sum && ./subset_sum
g++-16 -std=c++23 -O2 -Wall -Wextra subset_sum.cpp -o subset_sum && ./subset_sum
""")

Output

$ clang++ -std=c++23 -O2 -Wall -Wextra subset_sum.cpp -o subset_sum && ./subset_sum
vector<bool> loop   74695 totals reachable    143.57 ms
vector<char> loop   74695 totals reachable     60.77 ms
bitset shifts       74695 totals reachable      2.35 ms
$ g++-16 -std=c++23 -O2 -Wall -Wextra subset_sum.cpp -o subset_sum && ./subset_sum
vector<bool> loop   74695 totals reachable    151.17 ms
vector<char> loop   74695 totals reachable     62.14 ms
bitset shifts       74695 totals reachable      1.81 ms

This output changes from run to run: only the layout and the 74,695 totals are checked, because the times depend on the computer and on what else it is doing

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 subset_sum_session.py

All three find the same 74,695 reachable totals. In the recorded run the bitset needed about 2 ms with either compiler, while the loops needed tens to hundreds of milliseconds, the vector<bool> loop always the slowest: the bitset was at least twenty times faster than either loop, and usually far more. Your numbers will differ, but not the order. The loops do one step per weight and per total, about 100 million steps in all, while the bitset handles 64 totals per step: 1,563 words per weight, about 1.6 million word shifts and as many ors.

The vector<bool> loop is the slowest of the three because every access through its proxy has to find the word, build a mask: An integer with 1 bits exactly where an operation should act, such as 1u << 3 for bit 3. flags | mask sets those bits, flags & ~mask clears them, flags ^ mask flips them and flags & mask keeps only them. More in “C++ bitwise operators and the <bit> header” and test or set one bit. The bitset is ahead because the problem has the right shape, the same update for every total. A sieve of Eratosthenes: A way to find every prime up to n without dividing: for each number not yet crossed off, cross off its multiples from its square up. Up to ten million it makes 22,850,051 crossings, far fewer than trial division's divisions. More in “C++ practice set: loops, conditions and numbers”, which touches one bit at a time, has a different shape, and the exercise measures what that does to the ranking.

Key idea

When every flag gets the same update, a bitset does it a word at a time: one shift and one or moved 64 totals at once, about 1.6 million word operations where the loops took about 100 million single steps.

Checkpoint

Check: Whole-word shifts and ors

  1. Question 1 of 2 reach starts with only total 0. After reach |= reach << 3 and reach |= reach << 5, which totals are reachable?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 0, 3, 5 and 8

    Each shift-or keeps every total it had and adds the weight to each of them. 0 and 3 after the weight 3; then 0, 3, 5 and 8 after the weight 5.

  2. Question 2 of 2 A std::bitset<100001> is shifted left by one weight. How many 64-bit words does that shift go through?

    Type a number.

    A hint

    Divide the bits by 64 and round up.

    Show the answer: Question 2 of 2

    Answer: 1563 words

    100,001 bits need 100,001 / 64 = 1,562.5, so 1,563 words. For 1,000 weights that is about 1.6 million word shifts, against about 100 million single steps for the loops.

std::vector<bool> hands out proxies instead of references

The standard (section 24.3.12) declares a separate version of std::vector for bool, “to optimize space allocation”, and recommends a representation that packs the bits. Both libraries pack them, which is where the 12.5 MB above comes from. But a single bit has no address, so operator[] cannot return a bool&. It returns std::vector<bool>::reference, which the standard describes as a class that simulates a reference to a single bit: it converts to bool when read, and sets or clears its bit when assigned to. Most of the time the difference does not show. When it does, it surprises people:

proxy.cpp C++ · proxy.cpp
// What std::vector<bool>'s operator[] really returns, and the surprise it causes with auto.
#include <print>
#include <type_traits>
#include <vector>

int main() {
    std::vector<char> chars(4);
    std::vector<bool> flags(4);

    auto c = chars[0];   // a char: a copy of the element
    auto b = flags[0];   // a std::vector<bool>::reference: a small object that points at the bit
    chars[0] = 1;
    flags[0] = true;
    std::println("the copy from vector<char> is still {}", static_cast<int>(c));
    std::println("the \"copy\" from vector<bool> is now {}", static_cast<bool>(b));

    std::println("flags[0] is a bool&: {}", std::is_same_v<decltype(flags[0]), bool&>);
    std::println("flags[0] is a vector<bool>::reference: {}",
                 std::is_same_v<decltype(flags[0]), std::vector<bool>::reference>);

    bool copy = flags[1];   // naming the type converts the proxy to a plain bool
    flags[1] = true;
    std::println("bool copy = flags[1] stayed {}", copy);

    std::println("flags:        {}", flags);
    flags.flip();                                  // two members that only vector<bool> has: flip() ...
    std::println("after flip(): {}", flags);
    std::vector<bool>::swap(flags[0], flags[3]);   // ... and a swap of two elements through their proxies
    std::println("after swap(): {}", flags);
}
What vector<bool>'s operator[] returns Python · proxy_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra proxy.cpp -o proxy && ./proxy
g++-16 -std=c++23 -Wall -Wextra proxy.cpp -o proxy && ./proxy
""")

Output

$ clang++ -std=c++23 -Wall -Wextra proxy.cpp -o proxy && ./proxy
the copy from vector<char> is still 0
the "copy" from vector<bool> is now true
flags[0] is a bool&: false
flags[0] is a vector<bool>::reference: true
bool copy = flags[1] stayed false
flags:        [true, true, false, false]
after flip(): [false, false, true, true]
after swap(): [true, false, true, false]
$ g++-16 -std=c++23 -Wall -Wextra proxy.cpp -o proxy && ./proxy
the copy from vector<char> is still 0
the "copy" from vector<bool> is now true
flags[0] is a bool&: false
flags[0] is a vector<bool>::reference: true
bool copy = flags[1] stayed false
flags:        [true, true, false, false]
after flip(): [false, false, true, true]
after swap(): [true, false, true, false]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 proxy_session.py

auto b = flags[0]; did not copy a bool: auto deduced the proxy type, and the proxy still points into the vector, so writing flags[0] = true changed what b reads. The same line with std::vector<char> made a real copy. Naming the type, bool copy = flags[1];, converts the proxy at once and gives an ordinary bool. Step through those lines and watch what each name refers to:

Interactive · step by step What vector<bool>'s operator[] hands out Step through the program's lines and watch what each name refers to. The top left box is the vector's one word of bits.
What vector<bool>'s operator[] hands out, step 7 of 7. Showing flags: four bits (elements 0 to 3: 1 1 0 0), auto b = flags[0], auto c = chars[0], static_cast<bool>(b), bool copy = flags[1], bool& r = flags[0]. In focus: bool& r = flags[0].points at bit 0convertsflags: four bitselements 0 to 3: 1 1 0 0auto b = flags[0]a proxy: the word, bit 0auto c = chars[0]a char of its own: 0static_cast<bool>(b)reads bit 0 of the wordbool copy = flags[1]a plain bool: falsebool& r = flags[0]does not compile

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Four bits in one word. flags is a std::vector<bool> of four elements, all false. Both libraries pack them into the bits of one machine word, so no element has an address of its own.
  2. A proxy instead of bool&. auto b = flags[0] cannot get a bool&. operator[] returns std::vector<bool>::reference, a small object that holds where the word is and which bit is meant, and auto deduces that type.
  3. A char copy. The same line for a std::vector<char>, auto c = chars[0], copies the char: c is an object of its own, holding 0.
  4. Two writes. flags[0] = true sets bit 0 of the word, and chars[0] = 1 changes the vector of chars. c is a copy, so it still holds 0, as the program printed.
  5. The proxy reads the word. Reading b converts the proxy to bool: it reads bit 0 of the word, which is now 1. That is why the program printed that the "copy" from vector<bool> is now true.
  6. Naming the type copies. bool copy = flags[1] names the type, so the proxy is converted to a plain bool at once. After flags[1] = true, copy still held false.
  7. No bool& at all. bool& r = flags[0] has nothing to bind to: the proxy is a temporary, not a bool. Both compilers refused it, as they refused a bool* to an element and a for loop over bool&.

The last lines of the program show the two members that only vector<bool> has, flip() for every bit and a static swap for two elements.

C++26 deprecates the static swap

C++26 deprecates std::vector<bool>::swap(reference, reference) and gives the proxy a swap of its own (P3612R1), so from C++26 on two elements are swapped with swap(flags[0], flags[3]). This page’s programs are built as C++23, where the static member is the standard form.

Generic code is where the proxy really bites. A template written for std::vector<T> that takes a reference or a pointer to an element compiles for every T except bool:

proxy_errors.cpp C++ · proxy_errors.cpp
// Three lines that compile for std::vector<char> and fail for std::vector<bool>.
#include <vector>

template <class T>
void three_lines(std::vector<T>& v) {
    T& first = v[0];          // a reference to an element
    T* p = &v[1];             // a pointer to an element
    for (T& x : v) x = T{};   // a loop that modifies every element
    (void)first;
    (void)p;
}

int main() {
    std::vector<char> chars(8);
    three_lines(chars);   // fine
    std::vector<bool> flags(8);
    three_lines(flags);   // each of the three lines is an error
}
The same three lines for vector<bool> Python · proxy_errors_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra proxy_errors.cpp -o proxy_errors 2>&1 | grep error:
g++-16 -std=c++23 -Wall -Wextra proxy_errors.cpp -o proxy_errors 2>&1 | grep error:
""")

Output

$ clang++ -std=c++23 -Wall -Wextra proxy_errors.cpp -o proxy_errors 2>&1 | grep error:
proxy_errors.cpp:6:8: error: non-const lvalue reference to type 'bool' cannot bind to a temporary of type 'reference' (aka '__bit_reference<vector<bool, std::allocator<bool>>>')
proxy_errors.cpp:7:8: error: no viable conversion from '__bit_iterator<std::vector<bool, std::allocator<bool>>, false>' to 'bool *'
proxy_errors.cpp:8:13: error: non-const lvalue reference to type 'bool' cannot bind to a temporary of type 'reference' (aka 'std::__bit_reference<std::vector<bool>>')
$ g++-16 -std=c++23 -Wall -Wextra proxy_errors.cpp -o proxy_errors 2>&1 | grep error:
proxy_errors.cpp:6:17: error: cannot bind non-const lvalue reference of type 'bool&' to an rvalue of type 'bool'
proxy_errors.cpp:7:14: error: taking address of rvalue [-fpermissive]
proxy_errors.cpp:7:12: error: cannot convert 'std::vector<bool>::reference*' to 'bool*' in initialization
proxy_errors.cpp:8:5: error: cannot bind non-const lvalue reference of type 'bool&' to an rvalue of type 'bool'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 proxy_errors_session.py

Each compiler rejects the reference, the pointer and the loop that modifies through T&, and the messages name the proxy (__bit_reference in libc++) or say that the element is an rvalue: An xvalue or a prvalue: an expression whose object may be moved from, because nobody will look at it again. An rvalue reference, T&&, binds only to rvalues. More in “C++ value categories, std::move and copy elision”, a temporary with no address. For the same reason vector<bool> has no data(), and for (auto& b : flags) does not compile, while for (auto&& b : flags) does, because auto&& can bind to the temporary proxy.

Key idea

Every element of a std::vector<bool> is a bit with no address, so operator[] returns a proxy: auto copies the proxy and keeps a link to the vector, and bool&, bool* and data() do not exist.

Checkpoint

Check: The proxy of vector<bool>

  1. Question 1 of 2 For a std::vector<bool> flags, what type does auto b = flags[0]; give b?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: std::vector<bool>::reference, a proxy that points at the bit

    auto deduces the type of the expression, and flags[0] is a std::vector<bool>::reference. Writing flags[0] = true afterwards changed what b reads.

  2. Question 2 of 2 Which loop compiles and sets every flag of a std::vector<bool> flags?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: for (auto&& x : flags) x = true;

    Each element is handed out as a temporary proxy. auto&& binds to it and writes through it; auto& and bool& cannot bind, and a bool copy changes nothing.

Two threads that set different flags of one word can lose a write

The last difference cannot be seen in a single-threaded test. Two threads may normally write two different elements of one container at the same time, but the standard makes one exception, vector<bool>: its note in section 24.2.3 says that for a vector<bool> y, y[0] = true can race with y[1] = true, a Two threads using the same memory location at the same time, at least one of them writing, without a lock or an atomic operation to order them. In C++ a data race is undefined behaviour.. Both bits live in the same word, and a processor cannot write a single bit: each write reads the word, changes one bit and writes the word back. Step through the unlucky timing:

Interactive · message by message Two threads set two flags of one word Step through one timing of y[0] = true on thread 1 and y[1] = true on thread 2, where both flags of the vector<bool> y are bits of the same word.
Two threads set two flags of one word, step 6 of 6. Messages so far: the shared word to thread 1: read: both 0; the shared word to thread 2: read: both 0; thread 1 to the shared word: write: flag 0 is 1; thread 2 to the shared word: write: flag 1 is 1.thread 1holds a copy: 0, 0the shared wordflag 1: 1, flag 0: 0thread 2holds a copy: 0, 0read: both 0read: both 0write: flag 0 is 1write: flag 1 is 1flag 0 was set, then lost

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Two flags, one word. y is a std::vector<bool>, so its flags 0 and 1 are two bits of the same 64-bit word. Each thread writes only its own flag, which would be safe for any other container.
  2. Thread 1 reads. A processor cannot store a single bit, so y[0] = true starts by reading the whole word. Both flags are 0.
  3. Thread 2 reads. Before thread 1 writes anything back, thread 2 reads the same word for y[1] = true. It sees both flags at 0 too.
  4. Thread 1 writes back. Thread 1 sets flag 0 in its copy and writes the whole word back. Flag 0 is now 1.
  5. Thread 2 writes back. Thread 2 sets flag 1 in its own copy, which still has flag 0 at 0, and writes that word back. Flag 1 is now 1, and flag 0 is 0 again.
  6. A lost write. Both threads have finished, yet flag 0 is 0: thread 1's write is lost, and nothing reports it. With a std::vector<char>, each flag is a byte of its own and the two writes never meet.

Thread 2 wrote back a copy of the word that it had read before thread 1’s write, so flag 0 is clear again, and nothing reports it. The timing is rare, so a test can pass a thousand times and the bug still ships. A std::vector<char> does not have the problem: each flag is a byte of its own, a separate object, so the two writes never touch the same memory. A std::bitset has it too, because its set() and operator[] change one object that both threads share, and the library’s rule is that two threads calling non-const members of the same object race unless the class says otherwise.

Checkpoint

Check: Writes that share a word

  1. One question In the figure's timing, both threads read the word before either writes it back. What does the word hold when both have finished?

    Choose one answer.

    Show the answer: One question

    Answer: Flag 1 is 1 and flag 0 is 0: thread 1's write was lost

    Each write reads the whole word, changes one bit and writes the word back. The later write-back wins, and it was made from a copy without thread 1's bit.

Choose the container by what the flags must do

Four containers hold flags, and each fits a different need: the size known at compile time or only at run time, whole-set operations, real references, and threads that write different flags. Answer the questions for your own flags, and the table rules out what cannot work, with the reason:

Interactive · answer and compare Which container fits your flags? Answer the questions about the flags your program keeps. The table rules out what cannot do what you need, and says why.
Is the number of flags known when the program is compiled?
Will two threads write different flags of the same container at the same time?
Must code hold a reference or a pointer to a single flag, or pass data() to a C function?
Do you combine whole sets of flags with &, |, shifts or count()?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Four containers for flags against five needs, with this lesson's measurements
Container Size set at run time Memory, 100 million flags Whole-set operations References, pointers, data() Threads on different flags
std::bitset<N> Keeps each flag as: one bit; N fixed when compiling Poor N is a template argument, fixed when the program is compiled Good 12.5 MB, though as a local it would overflow the 8 MB stack Good &, |, ^, ~, shifts and count() work a 64-bit word at a time Poor operator[] returns a proxy, and there is no data() Poor set() on one shared bitset from two threads is a data race
std::vector<char> Keeps each flag as: one byte, an object of its own Good grows and shrinks like any vector Poor 100 MB, eight times the bits Poor one byte per step: the subset-sum loop took about 61 ms Good char&, char* and data() all work Good each flag is its own object, so different flags never race
std::vector<bool> Keeps each flag as: one bit behind a proxy Good grows like a vector, one bit per element Good 12.5 MB, packed like a bitset Mixed only flip() for every bit; no &, | or shifts, and the slowest loop Poor no bool&, no bool*, no data(); auto keeps a proxy Poor the standard lets y[0] = true race with y[1] = true
std::vector<std::uint64_t> Keeps each flag as: one bit of a word you index Good n flags need (n + 63) / 64 words, for any n Good 12.5 MB in 1,562,500 words Good your own loops over words handle 64 flags per step Mixed references to whole words, not to single flags Mixed safe while each thread owns whole words; a shared word races
How the answers decide
  • “Is the number of flags known when the program is compiled?” No, only at run time: rules out std::bitset<N>. N is a template argument, so a bitset cannot take a size that is known only at run time.
  • “Will two threads write different flags of the same container at the same time?” Yes: rules out std::bitset<N> and std::vector<bool> and adds a caveat to std::vector<std::uint64_t>. Both pack flags into shared words, and every write reads and writes back a whole word. With words of your own, give each thread whole words.
  • “Must code hold a reference or a pointer to a single flag, or pass data() to a C function?” Yes: rules out std::bitset<N>, std::vector<bool> and std::vector<std::uint64_t>. Only a vector<char> gives each flag an address of its own: the others hand out proxies or whole words.
  • “Do you combine whole sets of flags with &, |, shifts or count()?” Yes: rules out std::vector<char> and adds a caveat to std::vector<bool>. A vector<char> works one byte at a time, and a vector<bool> has only flip() for the whole set, through its slow proxies.
  • “Do you combine whole sets of flags with &, |, shifts or count()?” No and “Must code hold a reference or a pointer to a single flag, or pass data() to a C function?” No and “Will two threads write different flags of the same container at the same time?” No: adds a caveat to std::vector<char>. With no other need, memory decides: one bit per flag takes an eighth of what a byte per flag takes.

In short: std::bitset<N> when N is known at compile time and you want whole-set operations; std::vector<char> (or std::vector<std::uint8_t>) when code needs real references, data() or threads that write different flags; std::vector<bool> for the least memory at a run-time size, used only through its own interface; and a std::vector<std::uint64_t> with your own index arithmetic when you need whole-word operations on a run-time size, as the bitwise operators lesson shows.

Checkpoint

Check: Choosing a container for flags

  1. Question 1 of 2 Eight threads mark their own items done in shared flags, and the flags are later passed to a C function with data(). Which container fits?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: std::vector<char>

    Two needs decide it: threads that write different flags, and an address for the flags. Only a vector<char> (or vector<std::uint8_t>) gives every flag its own byte.

  2. Question 2 of 2 The number of seats in a hall is read from a file at start-up. Which container cannot hold one flag per seat?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: std::bitset<N>

    A bitset's size is part of its type. Every vector takes its size at run time, which is what a count read from a file needs.

Practise: a sieve on three containers

The sieve of Eratosthenes finds the primes up to n by crossing out the multiples of each prime, starting at its square. Write it three times in sieve.cpp, with a std::vector<bool>, a std::vector<char> and a std::bitset<10000001> that marks the composite numbers, and return how many primes are left. Checks in main test all three for n = 1, 2, 10, 100 and 1,000,000 (78,498 primes); once they pass, the program times the three versions up to ten million (664,579 primes).

sieve.cpp C++ · sieve.cpp
// Exercise: the sieve of Eratosthenes three times, on std::vector<bool>, std::vector<char> and std::bitset.
// Each function returns how many primes are <= n. Write the three bodies, then build with -O2 and run this file:
// every check must say "ok"; then the program times the three versions up to ten million.
#include <bitset>
#include <chrono>
#include <cstddef>
#include <format>
#include <memory>
#include <print>
#include <utility>
#include <vector>

std::size_t count_primes_vector_bool(std::size_t n) {
    (void)n;   // your code here: std::vector<bool> composite(n + 1)
    return 0;
}

std::size_t count_primes_vector_char(std::size_t n) {
    (void)n;   // your code here: std::vector<char> composite(n + 1)
    return 0;
}

constexpr std::size_t max_n = 10'000'000;

std::size_t count_primes_bitset(std::size_t n) {   // n <= max_n
    auto composite = std::make_unique<std::bitset<max_n + 1>>();   // 1.25 MB: on the heap, not the stack
    (void)n;   // your code here
    return 0;
}

int failures = 0;

void check(bool passed, const char* what) {
    std::println("{}  {}", passed ? "ok    " : "FAILED", what);
    if (!passed) ++failures;
}

template <class F>
void timed(const char* what, F count) {
    const auto start = std::chrono::steady_clock::now();
    const std::size_t primes = count(max_n);
    const std::chrono::duration<double, std::milli> ms = std::chrono::steady_clock::now() - start;
    std::println("{:<12} {} primes up to {} in {:.1f} ms", what, primes, max_n, ms.count());
}

int main() {
    for (auto [n, expected] : {std::pair<std::size_t, std::size_t>{1, 0}, {2, 1}, {10, 4}, {100, 25}, {1'000'000, 78'498}}) {
        const bool all_three = count_primes_vector_bool(n) == expected && count_primes_vector_char(n) == expected &&
                               count_primes_bitset(n) == expected;
        check(all_three, std::format("{} primes up to {}", expected, n).c_str());
    }
    std::println("{} of 5 checks failed", failures);
    if (failures != 0) return 1;
    timed("vector<bool>", count_primes_vector_bool);
    timed("vector<char>", count_primes_vector_char);
    timed("bitset", count_primes_bitset);
    return 0;
}

Before any function is written, the checks fail like this:

sieve.cpp as given Python · sieve_session.py
from session import run

run("""
clang++ -std=c++23 -O2 -Wall -Wextra sieve.cpp -o sieve && ./sieve
""")

Output

$ clang++ -std=c++23 -O2 -Wall -Wextra sieve.cpp -o sieve && ./sieve
ok      0 primes up to 1
FAILED  1 primes up to 2
FAILED  4 primes up to 10
FAILED  25 primes up to 100
FAILED  78498 primes up to 1000000
4 of 5 checks failed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sieve_session.py

You are done when all five checks say ok and the three times are printed. First hint: one function template, template <class Flags> std::size_t sieve(Flags& composite, std::size_t n), can do the work for all three, because all three support composite[i] and composite[i] = true. Start crossing out at i * i and stop the outer loop once i * i > n. Then compare your three times with the subset sums above, and work out why the ranking changes when every operation touches a single bit.

When the five checks pass, try the questions on bit order, memory, whole-word operations, proxies and threads. They start with what you know least, and every wrong option says what it overlooks:

Practice that adapts to you

Practise this lesson

15 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 15 What does this print?

    Read the code, then choose one answer.

    std::bitset<6> b("000101");
    std::println("{} {} {}", b.test(0), b.test(2), b.count());
    Show the answer: Question 1 of 15

    Answer: true true 2

    The last character of the string is bit 0, so "000101" sets bits 0 and 2: both tests are true and two bits are set. Reading the string from the left would give bits 3 and 5 instead.

  2. Question 2 of 15 What does count() return for a std::bitset<N>?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: How many of its bits are 1

    count() counts the 1 bits, size() returns N, and all(), any() and none() answer questions about all of them.

  3. Question 3 of 15 How many bytes does a std::bitset<65> take on both libraries of this lesson?

    Type a number.

    Show the answer: Question 3 of 15

    Answer: 16 bytes

    A bitset stores whole 64-bit words, and 65 bits need two of them: 16 bytes. std::bitset<1> to std::bitset<64> take 8.

  4. Question 4 of 15 For the same number of flags, how many times more memory did a std::vector<char> take than a std::vector<bool>?

    Type a number.

    Show the answer: Question 4 of 15

    Answer: 8 times

    A vector<char> spends a byte, 8 bits, on every flag, and a vector<bool> one bit: 100,000,000 against 12,500,000 bytes for a hundred million flags.

  5. Question 5 of 15 In the subset-sum program, what does reach << w give?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: Every reachable total t moved to t + w, all in one operation

    A left shift by w moves bit t to bit t + w. Or-ing the result into reach adds the weight w to every total that could be made before.

  6. Question 6 of 15 What goes wrong if the loop writes reach = reach << w; instead of reach |= reach << w;?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: Every total made without w is dropped, so only sums of all the weights so far are left

    The or keeps the subsets that leave the weight out. With plain assignment, every subset must use every weight, so for the weights 3 and 5 only total 8 would be left.

  7. Question 7 of 15 The weights are 3 and 3. Starting from total 0, which totals are reachable after both shift-ors?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: 0, 3 and 6

    Each shift-or uses one weight once. Two weights of 3 can give 0, 3 or 6, and the bits for those three totals are set.

  8. Question 8 of 15 What does this print?

    Read the code, then choose one answer.

    std::vector<bool> v(2);
    auto first = v[0];
    v[0] = true;
    std::println("{}", static_cast<bool>(first));
    Show the answer: Question 8 of 15

    Answer: true

    auto deduces std::vector<bool>::reference, the proxy, not bool. The proxy still points at bit 0 of v, so after v[0] = true it reads true. bool first = v[0]; would have made a copy that stays false.

  9. Question 9 of 15 Which of these lines compile when v is a std::vector<bool>?

    Choose every answer that is right.

    Show the answer: Question 9 of 15

    Answer:

    • for (auto&& x : v) x = true;
    • bool b = v[0];

    Reading through the proxy converts it to bool, and auto&& binds to the temporary proxy, so the first two compile. A bool& cannot bind to a proxy, and vector<bool> has no data() member.

  10. Question 10 of 15 Why does std::vector<bool>::operator[] return a proxy instead of a bool&?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: The elements are packed as bits, and a single bit has no address a reference could hold

    The standard's version of vector for bool packs the elements into bits. A reference must refer to an object with an address, so operator[] returns a small class that simulates a reference to one bit.

  11. Question 11 of 15 Two threads each set their own element of a shared container, one element 0 and the other element 1, with no lock. For which container does the standard allow this to be a data race?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: std::vector<bool>

    The standard requires containers to allow concurrent writes to different elements, with one exception, vector<bool>, whose elements share words: each write reads a whole word and writes it back.

  12. Question 12 of 15 Why can y[0] = true and y[1] = true race in a std::vector<bool> but not in a std::vector<char>?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: In vector<bool> both flags are bits of one word, and each write reads and writes back that whole word

    A processor stores whole bytes or words, never a single bit, so setting a bit is a read, a change and a write of the word. Bytes of a vector<char> are separate objects, written without touching their neighbours.

  13. Question 13 of 15 Two threads call b.set(0) and b.set(1) on one shared std::bitset<64> with no lock. Is that safe?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: No; both calls modify the same bitset object, which the library's rules make a data race

    The containers' exception for different elements does not cover a bitset. Two threads changing one object through its non-const members race, so guard the bitset with a mutex or give each thread its own.

  14. Question 14 of 15 A program keeps 64 permission flags per user, combines them with & and |, and counts them. One thread touches each user. Which container fits best?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: std::bitset<64>

    A fixed count, whole-set operations and no threads sharing a set: that is what std::bitset<N> is for, in one 64-bit word here.

  15. Question 15 of 15 One thread marks a billion items as seen, a count read at start-up, only through operator[]. Which container uses the least memory and still works?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: std::vector<bool>, at about 125 MB

    With one thread, no references and no whole-set operations, vector<bool>'s proxies cost nothing that matters, and its packing saves seven bits of every eight.

Interview questions

Fresher · What is the difference between std::bitset<N> and std::vector<bool>?

Both store flags as bits, but a std::bitset<N> has a size fixed at compile time and lives wherever it is declared, with no allocation, while a std::vector<bool> has a run-time size that can grow and keeps its bits on the heap. The bitset offers whole-set operations (&, |, ^, ~, shifts, count, all, any, none) and conversions to strings and integers. std::vector<bool> is a special version of std::vector whose operator[] returns a proxy object instead of bool&, so references, pointers to elements and data() do not work, auto copies the proxy rather than the value, and two threads writing different elements can race. Both index from bit 0, and for both an index outside the size in operator[] is undefined behaviour.

Interviewers often push on the edges of that answer next: an index one past the end, and a size that is not known when the program is compiled. Decide both:

Checkpoint

Interview round: two follow-up questions

  1. Question 1 of 2 Follow-up: std::bitset<8> b; b[9] = true; What does the standard say about the second statement?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It is undefined behaviour, while b.set(9) would throw std::out_of_range

    On a bitset, as on a vector<bool>, operator[] trusts its caller. The checked members, set(pos), reset(pos), flip(pos) and test(pos), throw out_of_range instead.

  2. Question 2 of 2 Follow-up: you need 10 million flags, a count known only at run time, and fast & and | between two such sets. What do you use?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A std::vector<std::uint64_t> with your own index arithmetic, combining whole words

    Neither standard bit container fits both needs. A vector of 64-bit words, indexed with i / 64 and i % 64, keeps a bit per flag at a run-time size and combines sets a word at a time.

The module’s bank holds two harder questions on these containers: worker threads that lose finished flags in a shared std::vector<bool>, and the subset-sum rewrite that ran about a hundred times faster:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    Eight worker threads mark finished items in a shared std::vector<bool> done(n), each thread writing only its own items. Occasionally an item that was finished is still marked as not done, and ThreadSanitizer reports a data race. No two threads touch the same index. Explain the bug and give two fixes.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    Rewriting a subset-sum check from a loop over std::vector<bool> to reach |= reach << w on a std::bitset made it about a hundred times faster. Explain where the speed comes from, estimate the work of both versions, and say when the same rewrite would not help.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • std::bitset<N> holds N bits in whole 64-bit words, and to_string() and the string constructor put bit 0 last; test() throws std::out_of_range, while operator[] does not check.
  • A hundred million flags take 12.5 MB as bits and 100 MB as bytes, and a large bitset belongs in static storage or on the heap, not on an 8 MB stack.
  • Whole-word operations made the subset-sum computation more than twenty times faster than the best per-flag loop, because one shift and one or update 64 totals at once.
  • std::vector<bool> packs bits too, but its operator[] returns a proxy: no bool&, no bool*, no data(), and auto keeps a link to the vector.
  • Two threads that set different flags of a vector<bool> or a bitset can lose a write, because each write reads and writes back a whole word; a std::vector<char> gives every flag its own byte.

Terms in this lesson

bit parallelism
Working on many flags at once by packing them into machine words, so that one bitwise instruction, such as a shift or an or, updates 64 of them. reach |= reach << w updates every total of a subset-sum problem this way.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.