C++ enum flags vs bitset

Viewed 13781

What are pros/cons of usage bitsets over enum flags?

namespace Flag {
    enum State {
        Read   = 1 << 0,
        Write  = 1 << 1,
        Binary = 1 << 2,
    };
}

namespace Plain {
    enum State {
        Read,
        Write,
        Binary,
        Count
    };
}

int main()
{
    {
        unsigned int state = Flag::Read | Flag::Binary;
        std::cout << state << std::endl;

        state |= Flag::Write;
        state &= ~(Flag::Read | Flag::Binary);
        std::cout << state << std::endl;
    } {
        std::bitset<Plain::Count> state;
        state.set(Plain::Read);
        state.set(Plain::Binary);
        std::cout << state.to_ulong() << std::endl;

        state.flip();
        std::cout << state.to_ulong() << std::endl;
    }

    return 0;
}

As I can see so far, bitsets have more convinient set/clear/flip functions to deal with, but enum-flags usage is a more wide-spreaded approach.

What are possible downsides of bitsets and what and when should I use in my daily code?

4 Answers

Some observations:

  • std::bitset< N > supports an arbitrary number of bits (e.g., more than 64 bits), whereas underlying integral types of enums are restricted to 64 bits;
  • std::bitset< N > can implicitly (depending on the std implementation) use the underlying integral type with the minimal size fitting the requested number of bits, whereas underlying integral types for enums need to be explicitly declared (otherwise, int will be used as the default underlying integral type);
  • std::bitset< N > represents a generic sequence of N bits, whereas scoped enums provide type safety that can be exploited for method overloading;
  • If std::bitset< N > is used as a bit mask, a typical implementation depends on an additional enum type for indexing (!= masking) purposes;

Note that the latter two observations can be combined to define a strong std::bitset type for convenience:

typename< Enum E, std::size_t N >
class BitSet : public std::bitset< N >
{
    ...

    [[nodiscard]]
    constexpr bool operator[](E pos) const;

    ...
};

and if the code supports some reflection to obtain the number of explicit enum values, then the number of bits can be deduced directly from the enum type.

  • scoped enum types do not have bitwise operator overloads (which can easily be defined once using SFINAE or concepts for all scoped and unscoped enum types, but need to be included before use) and unsoped enum types will decay to the underlying integral type;
  • bitwise operator overloads for enum types, require less boilerplate than std::bitset< N > (e.g., auto flags = Depth | Stencil;);
  • enum types support both signed and unsigned underlying integral types, whereas std::bitset< N > internally uses unsigned integral types (shift operators).

FWIIW, in my own code I mostly use std::bitset (and eastl::bitvector) as private bit/bool containers for setting/getting single bits/bools. For masking operations, I prefer scoped enum types with explicitly defined underlying types and bitwise operator overloads.

Related