Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ enum flags vs bitset

Tags:

c++

enums

bitset

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?

like image 857
Nikolai Shalakin Avatar asked Aug 07 '17 07:08

Nikolai Shalakin


2 Answers

Both std::bitset and c-style enum have important downsides for managing flags. First, let's consider the following example code :

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

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

void f(int);
void g(int);
void g(Flag::State);
void h(std::bitset<sizeof(Flag::State)>);

namespace system1 {
    Flag::State getFlags();
}
namespace system2 {
    Plain::State getFlags();
}

int main()
{
    f(Flag::Read);  // Flag::Read is implicitly converted to `int`, losing type safety
    f(Plain::Read); // Plain::Read is also implicitly converted to `int`

    auto state = Flag::Read | Flag::Write; // type is not `Flag::State` as one could expect, it is `int` instead
    g(state); // This function calls the `int` overload rather than the `Flag::State` overload

    auto system1State = system1::getFlags();
    auto system2State = system2::getFlags();
    if (system1State == system2State) {} // Compiles properly, but semantics are broken, `Flag::State`

    std::bitset<sizeof(Flag::State)> flagSet; // Notice that the type of bitset only indicates the amount of bits, there's no type safety here either
    std::bitset<sizeof(Plain::State)> plainSet;
    // f(flagSet); bitset doesn't implicitly convert to `int`, so this wouldn't compile which is slightly better than c-style `enum`

    flagSet.set(Flag::Read);    // No type safety, which means that bitset
    flagSet.reset(Plain::Read); // is willing to accept values from any enumeration

    h(flagSet);  // Both kinds of sets can be
    h(plainSet); // passed to the same function
}

Even though you may think those problems are easy to spot on simple examples, they end up creeping up in every code base that builds flags on top of c-style enum and std::bitset.

So what can you do for better type safety? First, C++11's scoped enumeration is an improvement for type safety. But it hinders convenience a lot. Part of the solution is to use template-generated bitwise operators for scoped enums. Here is a great blog post which explains how it works and also provides working code : https://www.justsoftwaresolutions.co.uk/cplusplus/using-enum-classes-as-bitfields.html

Now let's see what this would look like :

enum class FlagState {
    Read   = 1 << 0,
    Write  = 1 << 1,
    Binary = 1 << 2,
};
template<>
struct enable_bitmask_operators<FlagState>{
    static const bool enable=true;
};

enum class PlainState {
    Read,
    Write,
    Binary,
    Count
};

void f(int);
void g(int);
void g(FlagState);
FlagState h();

namespace system1 {
    FlagState getFlags();
}
namespace system2 {
    PlainState getFlags();
}

int main()
{
    f(FlagState::Read);  // Compile error, FlagState is not an `int`
    f(PlainState::Read); // Compile error, PlainState is not an `int`

    auto state = FlagState::Read | FlagState::Write; // type is `FlagState` as one could expect
    g(state); // This function calls the `FlagState` overload

    auto system1State = system1::getFlags();
    auto system2State = system2::getFlags();
    if (system1State == system2State) {} // Compile error, there is no `operator==(FlagState, PlainState)`

    auto someFlag = h();
    if (someFlag == FlagState::Read) {} // This compiles fine, but this is another type of recurring bug
}

The last line of this example shows one problem that still cannot be caught at compile time. In some cases, comparing for equality may be what's really desired. But most of the time, what is really meant is if ((someFlag & FlagState::Read) == FlagState::Read).

In order to solve this problem, we must differentiate the type of an enumerator from the type of a bitmask. Here's an article which details an improvement on the partial solution I referred to earlier : https://dalzhim.github.io/2017/08/11/Improving-the-enum-class-bitmask/ Disclaimer : I'm the author of this later article.

When using the template-generated bitwise operators from the last article, you will get all of the benefits we demonstrated in the last piece of code, while also catching the mask == enumerator bug.

like image 52
Dalzhim Avatar answered Oct 20 '22 00:10

Dalzhim


Do you compile with optimization on? It is very unlikely that there is a 24x speed factor.

To me, bitset is superior, because it manages space for you:

  • can be extended as much as wanted. If you have a lot of flags, you may run out of space in the int/long long version.
  • may take less space, if you only use just several flags (it can fit in an unsigned char/unsigned short - I'm not sure that implementations apply this optimization, though)
like image 31
geza Avatar answered Oct 19 '22 23:10

geza