#035 – How to use std::bitset for EFFORTLESS bit manipulation

Introduction

On modern architectures, the smallest unit of memory is a byte.

However, if we’re storing a bool, using a byte of memory to store it as a bit (pun intended) is wasteful. The other 7 bits are unused.

Additionally, using ints with the raw bitwise operators (|, &, ^, <<) can be awkward.

Fortunately, C++ provides a solution in <bitset>.

This allows container not only allows us to pack 8 bits into a byte it grants us access to a suite of methods that performing bit manipulation a breeze.

Let’s go!

Performing bit manipulation using std::bitset

std::bitset exposes four single-bit operations:

Method nameDescription
test(i)Allows us to query whether a bit is 0 or 1.
set(i)Allows us to turn a bit on. This does nothing if the bit is already on.
reset(i)Allows us to turn a bit off. This does nothing if the bit is already off.
flip(i)Allows us to flip a bit value from 0 to 1, or from 1 to 0.

As you can see, the methods grant us a very fine level of control over the bits.

Bits are indexed from 0, with bit 0 being the least significant bit — the same convention as array indexing.

Taking std::bitset for a testdrive

Firstly, we need to import the library, <bitset>

We instantiate a variable of type std::bitset<X>. The number in the angled brackets is the amount of bits you would like to store.

#include <bitset>
#include <iostream>

int main()
{
    std::bitset<4> bits{ 0b0101 };
    std::cout << "Initial:    " << bits << '\n';

    bits.reset(0);
    std::cout << "Reset 0:    " << bits << '\n';

    bits.set(3);
    std::cout << "Set 3:      " << bits << '\n';

    bits.flip(3);
    std::cout << "Flip 3:     " << bits << '\n';
}

// Output
Initial:    0101
Reset 0:    0100
Set 3:      1100
Flip 3:     0100

The bitset prints with the MSB on the left, which aligns with convention.

Each method takes a bit index and either reports or modifies that single position.

Increasing the semantics

Passing in the bit positions as arguments is a bit (pun intended) cryptic.

myByte.set(3);
// What does 3 mean? 

Fortunately, we can do better.

Doing Better – An RPG Example

In an RPG, an enemy can have numerous statuses – poisoned, burning, frozen etc.. Let’s store this in in unscoped enum StatusEffect.

NOTE: A regular enum is used to implicitly convert the enumerations into an int. These will be arguments into the methods of std::bitset.

Our bits will represent whether status are active or inactive.

#include <bitset>
#include <iostream>

int main()
{
    enum StatusEffect
    {
        isPoisoned,   // 0
        isBurning,    // 1
        isFrozen,     // 2
        isStunned,    // 3
        isInvisible,  // 4
        hasShield,    // 5
        isSilenced,   // 6
        isRooted      // 7
    };

    std::bitset<8> status{ 0b0010'0001 };
    // bit 0 -> isPoisoned
    // bit 5 -> hasShield

    status.set(isStunned);      // apply stun
    status.flip(isInvisible);   // toggle invisibility on
    status.reset(isPoisoned);   // cure poison

    std::cout << std::boolalpha;
    std::cout << "Stunned:  " << status.test(isStunned)  << '\n';
    std::cout << "Shielded: " << status.test(hasShield)  << '\n';
    std::cout << "Poisoned: " << status.test(isPoisoned) << '\n';
}

// Output
Stunned:  true
Shielded: true
Poisoned: false

It’s much easier to understand statusEffects.set(StatusEffect::isStunned) rather than statusEffects.set(3).

Thus, we shift from working with bit positions to identifiers. This allows us to make the code more semantic and less error-prone.

You’ve probably noticed that the indexed overloads methods of std::bitset apply to a single bit position.

NOTE: std::bitset methods only manipulate one position at a time. To set or clear multiple bits in a single operation, use bit masks — covered in Nibble #036

If we want to set or clear multiple bits in a single operation, bit masks. This is covered in #037 – How to use bit masking to extract meaningful data

Querying std::bitset

std::bitset provides five query methods for inspecting the state as a whole:

Method nameDescription
size()Returns the number of bits in the bitset.
count()Returns number of bits currently set to 1.
all()Returns true if every bit is 1.
any()Returns true if at least one bit is 1.
none()Returns true if every bit is 0.
#include <bitset>
#include <iostream>

int main()
{
    std::bitset<8> bits{ 0b0000'0101 };

    std::cout << std::boolalpha;
    std::cout << "Size:     " << bits.size()  << '\n';
    std::cout << "Set bits: " << bits.count() << '\n';
    std::cout << "All:      " << bits.all()   << '\n';
    std::cout << "Any:      " << bits.any()   << '\n';
    std::cout << "None:     " << bits.none()  << '\n';
}

// Output
Size:     8
Set bits: 2
All:      false
Any:      true
None:     false

Takeaway

std::bitset<N> packs N bits into a fixed-size container. It comes out of the box with a suite of bit manipulation methods.

Reach for std::bitset whenever you find yourself maintaining a fixed set of boolean flags, status bits, or feature toggles. Pair it with an unscoped enum of bit positions and suddenly you’ve raised the semantics. status.set(isStunned) reads as plain English, where status |= 1 << 3 does not.