Historia: C++98 wprowadziło std::vector<bool> jako specjalizowany kontener do przechowywania wartości bool w skompresowanej reprezentacji bitowej, alokując jeden bit na wartość logiczną zamiast jednego bajtu. Ta decyzja projektowa miała na celu zapewnienie znacznych oszczędności pamięci – osiem razy bardziej kompaktowej niż std::vector<char> – co było kluczowe dla wczesnych aplikacji przetwarzających duże zestawy bitowe. Jednak, ponieważ pojedyncze bity nie mają odrębnych adresów pamięci, referencje w C++ nie mogą im odpowiadać, co wymaga stworzenia klasy referencji proxy w celu symulacji semantyki referencji.
Problem: Standard C++ wymaga, aby standardowe kontenery dostarczały prawdziwe referencje (bool&) jako ich typ reference, ale std::vector<bool> zwraca obiekty proxy (zazwyczaj nazywane reference). To naruszenie łamie wymagania koncepcji Container, co powoduje, że ogólne algorytmy wykorzystujące auto& lub std::is_same_v< decltype(vec[0]), bool& > nie kompilują się lub zachowują się nieprzewidywalnie. W konsekwencji kod, który oczekuje ciągłych układów pamięci lub arytmetyki wskaźników na elementach, napotyka nieokreślone zachowanie lub błędy logiczne, gdy stosuje tę specjalizację.
std::vector<bool> bits = {true, false}; auto& ref = bits[0]; // ref jest proxy, a nie bool& // bool* p = &bits[0]; // BŁĄD: brak odpowiedniej konwersji
Rozwiązanie: Komisja utrzymała tę specjalizację pomimo naruszenia semantycznego, ponieważ korzyści z efektywności pamięci przewyższały ścisłą zgodność dla konkretnego przypadku użycia. Programiści wymagający standardowej semantyki kontenerów powinni unikać std::vector<bool> i używać alternatyw takich jak std::vector<char>, std::deque<bool> lub boost::dynamic_bitset, które zapewniają prawdziwe referencje kosztem efektywności pamięci.
Startup zajmujący się analizą danych wdrożył algorytm wyrównywania sekwencji genomowych, przechowując miliardy flag mutacji w std::vector<bool>, aby zmaksymalizować wykorzystanie RAM. Ich ogólna funkcja szablonowa process_flags akceptowała każdy kontener i używała auto& flag = container[i] do przełączania bitów, zakładając semantykę bool&. Podczas integracji z zewnętrzną biblioteką do przetwarzania równoległego, kompilacja zakończyła się niepowodzeniem, ponieważ system cech biblioteki wykrył, że decltype(flag) nie było typem referencyjnym, odrzucając std::vector<bool> jako nieobsługiwane.
Dyskutowano trzy rozwiązania. Po pierwsze, refaktoryzacja systemu do używania std::vector<uint8_t>. Zalety: natychmiastowa zgodność z całym ogólnym kodem i gwarancje prawdziwej referencji. Wady: zużycie pamięci wzrosło o 800%, przekraczając dostępny RAM na ich serwerach. Po drugie, wyraźna specjalizacja process_flags dla std::vector<bool> przy użyciu metod klasy proxy. Zalety: zachowanie efektywności pamięci. Wady: wymaga utrzymania dwóch ścieżek kodu i ujawnia szczegóły implementacyjne, naruszając enkapsulację. Po trzecie, migracja do boost::dynamic_bitset, który wyraźnie obsługuje bity bez udawania standardowego kontenera. Zalety: jasne API, prawdziwa manipulacja bitami i brak niespodzianek związanych z proxy. Wady: dodaje zewnętrzną zależność i wymaga zmian w API w całej podstawie kodu.
Zespół wybrał boost::dynamic_bitset, ponieważ wymagania biblioteki zewnętrznej były niezmienne, a ograniczenia pamięci były nieprzekraczalne. Po migracji system niezawodnie przetwarzał dane genomowe bez błędów kompilacji związanych z typami, osiągając zarówno wydajność, jak i poprawność.
&vec[0] powoduje błąd kompilacji lub nieprawidłowy wskaźnik, gdy vec to std::vector<bool>?Ponieważ vec[0] zwraca tymczasowy obiekt proxy, a nie lvalue bool. Wzięcie adresu tego tymczasowego skutkuje wskaźnikiem do krótkotrwałej instancji proxy, a nie do rzeczywistego magazynu bitów. W przeciwieństwie do standardowych kontenerów, w których elementy są obiektami kontyngentowymi, bity w std::vector<bool> nie mają adresowalnych lokalizacji, co czyni operacje arytmetyki wskaźników i operacje pobierania adresów semantycznie nieprawidłowymi.
std::vector<bool> vec(10); // bool* p = &vec[0]; // Niewłaściwa forma
Kiedy ogólna lambda wychwytuje [&] i operuje na container[i], perfekcyjne przekazywanie przez decltype(auto) wydedukowuje typ proxy, a nie bool&. Jeśli lambda przekazuje to do funkcji oczekującej bool&, obiekt proxy (który zazwyczaj jest tymczasowy lub zawiera wewnętrzne maski bitowe) degeneruje się lub kopiowany jest nieprawidłowo, co prowadzi do wprowadzenia zmian do tymczasowych kopii zamiast do oryginalnych elementów kontenera, powodując cichą utratę danych.
auto lambda = [](auto&& x) { return std::forward<decltype(x)>(x); }; std::vector<bool> vec = {false}; auto&& ref = lambda(vec[0]); // ref związany z proxy ref = true; // Może nie zmodyfikować vec[0], jeśli proxy jest tymczasową kopią
Operator operator* iteratora zwraca proxy przez wartość, łamiąc wymaganie, że *it zwraca referencję lvalue do typu elementu dla iteratorów kontyngentowych. Chociaż iteratory std::vector<bool> wspierają arytmetykę w czasie stałym (it += n), podstawa magazynowa nie jest ciągłą tablicą obiektów bool, co uniemożliwia prawidłowe użycie std::to_address(it) lub optymalizacji opartych na wskaźnikach, które zakładają &*(it + n) == &*it + n, łamiąc ścisłe założenia o aliasingu i prefetching lini cache.
static_assert(!std::contiguous_iterator<std::vector<bool>::iterator>); // Iterator jest RandomAccess, ale nie Contiguous