Флаги переполнения С++ vs bitset

Каковы плюсы и минусы использования битов-битов над флагами enum?

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;
}

Как я вижу до сих пор, биты имеют более удобные функции set/clear/flip для обработки, но использование флажков-переходов является более широко распространенным подходом.

Каковы возможные недостатки битов и что и когда я должен использовать в своем ежедневном коде?

Ответы

Ответ 1

Собираетесь ли вы с оптимизацией? Очень маловероятно, что существует коэффициент скорости 24x.

Для меня битрейт превосходен, потому что он управляет пространством для вас:

  • может быть расширено столько, сколько нужно. Если у вас много флагов, вы можете пробежать в версии int/long long.
  • может занимать меньше места, если вы используете только несколько флагов (он может поместиться в unsigned char/unsigned short - я не уверен, что реализации используют эту оптимизацию, хотя)

Ответ 2

Оба std::bitset и c-style enum имеют важные недостатки для управления флагами. Во-первых, рассмотрим следующий пример кода:

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 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
}

Несмотря на то, что вы можете подумать, что эти проблемы легко обнаружить на простых примерах, они заканчиваются, ползая в каждой базе кода, которая строит флаги поверх c-style enum и std::bitset.

Итак, что вы можете сделать для лучшей безопасности типов? Во-первых, перечисление С++ 11 scoped является улучшением безопасности типов. Но это затрудняет удобство. Часть решения заключается в использовании побитовых операторов, сгенерированных шаблоном, для скопированных перечислений. Вот отличный пост в блоге, в котором объясняется, как он работает, а также предоставляет рабочий код: https://www.justsoftwaresolutions.co.uk/cplusplus/using-enum-classes-as-bitfields.html

Теперь посмотрим, как это будет выглядеть:

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 = Flag::Read | Flag::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
}

В последней строке этого примера показана одна проблема, которая по-прежнему не может быть обнаружена во время компиляции. В некоторых случаях сравнение для равенства может быть действительно желательным. Но большую часть времени, что на самом деле означает if ((someFlag & FlagState::Read) == FlagState::Read).

Чтобы решить эту проблему, мы должны различать тип перечислителя от типа битовой маски. Здесь статья, в которой подробно рассматривается частичное решение, о котором я говорил ранее: https://dalzhim.github.io/2017/08/11/Improving-the-enum-class-bitmask/ Отказ от ответственности: я являюсь автором этой более поздней статьи.

При использовании порожденных по шаблону операторов из последней статьи вы получите все преимущества, которые мы продемонстрировали в последнем фрагменте кода, а также поймаем ошибку mask == enumerator.

Ответ 3

(включен режим объявления) Вы можете получить оба: удобный интерфейс и максимальную производительность. И безопасность типа. https://github.com/oliora/bitmask