Boost.DynamicBitset

Boost.DynamicBitset is a portable library that provides a set of bits.

The set (dynamic_bitset) provides access to the value of individual bits via operator[] and provides all of the bitwise operators that one can apply to builtin integers, such as operator& and operator<<. The number of bits can change at runtime.

dynamic_bitset is nearly identical to std::bitset. The difference is that the size of a dynamic_bitset (the number of bits) can change at runtime, whereas the size of a std::bitset is specified at compile-time through an integer template parameter.

The main problem that dynamic_bitset is designed to solve is that of representing a subset of a finite set. Each bit represents whether an element of the finite set is in the subset or not. As such the bitwise operations of dynamic_bitset, such as operator& and operator|, correspond to set operations, such as intersection and union.

Definitions

Each bit represents either the Boolean value true or false (1 or 0). To set a bit is to assign it 1. To clear or reset a bit is to assign it 0. To flip a bit is to change the value to 1 if it was 0 and to 0 if it was 1. Each bit has a non-negative position. A bitset x contains x.size() bits, with each bit assigned a unique position in the range [0, x.size()). The bit at position 0 is called the least significant bit and the bit at position size() - 1 is the most significant bit. When converting an instance of dynamic_bitset to or from an unsigned integer n, the bit at position i of the bitset has the same value as (n >> i) & 1.

Rationale

Because of the proxy reference type, dynamic_bitset is not a Container and its iterators do not satisfy the requirements for a LegacyForwardIterator. This means that, formally, its iterators are not usable with many standard algorithms. In practice, algorithms such as std::reverse() and std::sort() generally work, because they only need to read, assign and swap the elements, and the proxy reference supports all of that (swapping is done by an unqualified call to swap()). Moreover, dynamic_bitset provides C++20 iterators, which can be used with ranges, including the algorithms which modify the elements, such as std::ranges::fill(), std::ranges::reverse() and std::ranges::sort().

Some people prefer the name "toggle" to "flip". The name "flip" was chosen because that is the name used in std::bitset. In fact, most of the function names for dynamic_bitset were chosen for this reason.

dynamic_bitset does not throw exceptions when a precondition is violated (as is done in std::bitset). Instead BOOST_ASSERT() is used. See the guidelines for Error and Exception Handling for the explanation. Note that, consistently with this, the documentation of the member functions doesn’t use the term "precondition" for conditions that cause an exception to be emitted (the C++ standard uses the term "requires" in such cases).

The functions which have a precondition (e.g., test() and operator[]) aren’t declared noexcept, even if they can’t throw: so, an assertion handler which throws (see BOOST_ENABLE_ASSERT_HANDLER) can report a violation, e.g. in a test. The functions which have no precondition and can’t throw, such as clear(), count(), find_first() and the comparison operators, are noexcept. The default constructor, the constructor from an allocator, the move operations and swap() are noexcept if the corresponding operations of the underlying container are.

Constant evaluation

In C++20 and later, all the functions of dynamic_bitset, including those of its iterators and of its reference class, are constexpr, except the stream inserter and extractor, and the support for hashing and for Boost.Serialization. A call to them can be a constant expression only if the underlying container supports constant evaluation. So, if the standard library supports constant evaluation of std::vector (i.e., if it defines __cpp_lib_constexpr_vector), a dynamic_bitset which uses the default underlying container can be created, modified and destroyed during constant evaluation:

constexpr bool
f()
{
    boost::dynamic_bitset<> b( 70 );
    b.set( 3 ).set( 69 );
    return b.count() == 2 && b.find_next( 3 ) == 69;
}

static_assert( f() );

The constructors from a std::basic_string and to_string() also need a standard library which supports constant evaluation of std::basic_string (__cpp_lib_constexpr_string). As with std::vector, any memory allocated during a constant evaluation must be deallocated before it ends: so, in general, a dynamic_bitset can’t be a constexpr variable. A call which throws an exception (e.g., at() with an invalid position) is not a constant expression, and neither is one which violates a precondition checked by BOOST_ASSERT(), if assertions are enabled. If AllocatorOrContainer is a user-supplied container, constant evaluation is possible if that container supports it, even if std::vector doesn’t; if it’s a user-supplied allocator, that allocator must support it too.

With libstdc++ in debug mode (i.e., if _GLIBCXX_DEBUG is defined), some of the functions can’t be used in constant expressions, at least as of GCC 15, because the checks which that mode adds to standard algorithms such as std::fill() aren’t constexpr.

Serialization

dynamic_bitset can be serialized with Boost.Serialization (see also serialize_impl). To do so, include <boost/dynamic_bitset/serialization.hpp> and make the underlying buffer serializable, too. For the default buffer (a std::vector), this means including <boost/serialization/vector.hpp>:

#include <boost/dynamic_bitset/serialization.hpp>
#include <boost/serialization/vector.hpp>

<boost/dynamic_bitset/serialization.hpp> doesn’t include any Boost.Serialization header itself, so that this library doesn’t depend on Boost.Serialization. If the second template argument of dynamic_bitset is a container, the Boost.Serialization support for that container must be visible instead. Boost.Serialization doesn’t provide it for every container (e.g., it doesn’t provide it for boost::container::small_vector): in that case, you have to write it yourself.

An archive holds the size of the bitset followed by its blocks, so it must be loaded into a dynamic_bitset with the same Block type as the saved one (a mismatch is not always detected). In particular, text and XML archives are portable only between platforms where Block has the same width: for portable archives, use a fixed-width type such as std::uint32_t rather than the default unsigned long.

Loading checks that the archive holds a valid bitset, and throws std::invalid_argument if it doesn’t. If loading throws, for this or any other reason, the bitset being loaded keeps its previous value, unless the archive throws after reading all of the data of the bitset (e.g., because an XML end tag is missing): then, it holds the loaded value.

Because loading validates the archive, a non-empty dynamic_bitset can’t be transmitted as a Boost.MPI skeleton (with boost::mpi::skeleton()): the skeleton holds the number of blocks, but not the size of the bitset, so loading it on the receiving side throws std::invalid_argument.

Acknowledgments

We would like to thank the Boost community for putting in the time to review and accept this library. This library is much better than it ever would have been due to all the suggestions from Boost members. We especially thank Matt Marcus for taking on the task of review manager. Also, a special thanks goes to James Kanze for his invaluable help with the internationalization issues.