C++'s vector<bool> Proxy Trap: When auto Captures a Reference You Didn't Ask For

2026-05-28

This function is supposed to atomically swap a flag to false and return whatever it was before — a classic test-and-clear primitive. The caller uses the returned value to decide whether to run a one-time initialization.

#include <vector>
#include <iostream>

bool test_and_clear(std::vector<bool>& flags, std::size_t i) {
    auto previous = flags[i];   // snapshot the old value
    flags[i] = false;           // clear it
    return previous;            // return what it WAS
}

int main() {
    std::vector<bool> needs_init = {true, true, true};

    if (test_and_clear(needs_init, 0)) {
        std::cout << "initializing slot 0\n";
    } else {
        std::cout << "slot 0 already initialized\n";
    }
}

The vector clearly starts with true at index 0. So test_and_clear should snapshot true, write false, and return true. Initialization should run.

It doesn't. The program prints "slot 0 already initialized". And every subsequent call agrees — the flag was apparently never set.

The Bug

std::vector<bool> is the standard library's most infamous wart: it isn't a container of bool. It's a bit-packed specialization, and operator[] returns a proxy object of type std::vector<bool>::reference, not a bool. That proxy holds a pointer to the underlying word plus a bitmask — it's a live reference to the bit.

When you write auto previous = flags[i];, type deduction picks std::vector<bool>::reference, not bool. You did not snapshot a value. You bound a proxy that aliases the very bit you're about to overwrite. The next line, flags[i] = false;, flips the bit — and previous sees the new value, because it is the bit. The return previous; then implicitly converts the (now-cleared) proxy to bool and reports false.

This is the same shape of bug as a dangling iterator, but with a wonderful twist: the proxy doesn't even need the original vector to be reallocated to mislead you. Mutation alone is enough, because the proxy never had its own storage.

The fix is to force the conversion to bool at capture time, before the mutation:

bool test_and_clear(std::vector<bool>& flags, std::size_t i) {
    bool previous = flags[i];   // explicit type forces a copy to bool
    flags[i] = false;
    return previous;
}

Or, if you're committed to auto, cast explicitly: auto previous = static_cast<bool>(flags[i]);. Either way, the goal is to materialize the bit's value into a real bool variable before you go scribble on its storage.

Related landmines from the same specialization: auto& r = v[0]; won't even bind because the proxy is a prvalue; std::vector<bool> doesn't satisfy the Container requirements (no data() returning bool*); range-for with auto& gives you the proxy too, with the same aliasing hazard. The standards committee has tried to deprecate this specialization for over a decade and failed. Until they succeed, prefer std::vector<char>, std::deque<bool>, or std::bitset when you want predictable semantics — and never let auto deduce a bit reference behind your back.

Key Takeaway: vector<bool>'s operator[] returns a live proxy, not a bool — so auto x = v[i]; captures a reference to the bit you're about to overwrite, not a snapshot of its value.

All newsletters