c++ - В поисках лучшего перечисления битового флага




enums bit-manipulation (3)

Итак, мы находимся на C ++ 17, и до сих пор нет удовлетворительного ответа на действительно отличный интерфейс битовых флагов в C ++.

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

У нас есть enum class который решает проблему области имен, так что их значения должны быть явно названы MyEnum::MyFlag или даже MyClass::MyEnum::MyFlag , но они неявно преобразуются в свой базовый тип, поэтому их нельзя использовать как битовые флаги без бесконечного литья взад и вперед.

И, наконец, у нас есть старые битовые поля из C такие как:

struct FileFlags {
   unsigned ReadOnly : 1;
   unsigned Hidden : 1;
   ...
};

Недостатком которого является отсутствие хорошего способа инициализации себя как целого - приходится прибегать к использованию memset или приведению адреса или тому подобное, чтобы перезаписать все значение или инициализировать его все сразу или иным образом манипулировать несколькими битами одновременно. Он также страдает от невозможности назвать значение данного флага, в отличие от его адреса - поэтому нет имени, представляющего 0x02, тогда как при использовании перечислений такое имя существует, поэтому с помощью перечислений легко назвать комбинацию из флаги, такие как FileFlags::ReadOnly | FileFlags::Hidden FileFlags::ReadOnly | FileFlags::Hidden - просто нет хорошего способа сказать так много для битовых полей.

Кроме того, у нас все еще есть простой constexpr или #define для именования битовых значений, а затем мы просто не используем перечисления вообще. Это работает, но полностью отделяет битовые значения от базового типа битового флага. Возможно, это в конечном счете не самый плохой подход, особенно если значения битовых флагов являются constexpr внутри структуры, чтобы дать им собственную область имен?

struct FileFlags {
    constexpr static uint16_t ReadOnly = 0x01u;
    constexpr static uint16_t Hidden = 0x02u;
    ...
}

Таким образом, в настоящее время у нас есть много методов, и ни один из них не дает в итоге действительно убедительного способа сказать,

Вот тип, который имеет следующие допустимые битовые флаги, имеет собственную область имен, и эти биты и тип должны свободно использоваться со стандартными побитовыми операторами, такими как | & ^ ~, и они должны быть сопоставимы с целочисленными значениями, такими как 0, а результат любых побитовых операторов должен оставаться именованным типом, а не переходить в интеграл

Все это говорит о том, что существует множество попыток создать вышеуказанную сущность в C ++ -

  1. Команда Windows OS разработала простой макрос, который генерирует код C ++ для определения необходимых пропущенных операторов для данного типа перечисления DEFINE_ENUM_FLAG_OPERATORS(EnumType) который затем определяет operator | & ^ ~ и связанные операции назначения, такие как | = и т. д.
  2. У 'grisumbras' есть общедоступный проект GIT для включения семантики битовых флагов с перечислениями области действия, который использует метапрограммирование enable_if чтобы позволить данному перечислению преобразовывать в тип битового флага, который поддерживает отсутствующие операторы, и снова молча.
  3. Не зная вышеизложенного, я написал относительно простую оболочку bit_flags, которая определяет для себя все побитовые операторы, так что можно использовать bit_flags<EnumType> flags а затем flags имеет битовую семантику. Чего это не может сделать, так это позволить перечисляемой базе фактически правильно обрабатывать побитовые операторы напрямую, поэтому вы не можете сказать EnumType::ReadOnly | EnumType::Hidden EnumType::ReadOnly | EnumType::Hidden даже при использовании bit_flags<EnumType> потому что само базовое перечисление все еще не поддерживает необходимые операторы. Я должен был в конечном итоге сделать то же самое, что и # 1 и # 2 выше, и включить operator | (EnumType, EnumType) operator | (EnumType, EnumType) для различных побитовых операторов, требуя от пользователей объявить специализацию для template <> struct is_bitflag_enum<EnumType> : std::true_type {}; для их перечисления, такого как template <> struct is_bitflag_enum<EnumType> : std::true_type {};

В конечном счете, проблема с № 1, № 2 и № 3 заключается в том, что невозможно (насколько я знаю) определить отсутствующие операторы в самом перечислении (как в № 1) или определить необходимый тип активатора ( например, template <> struct is_bitflag_enum<EnumType> : std::true_type {}; как в # 2 и частично # 3) в области видимости класса. Это должно происходить вне класса или структуры, поскольку в C ++ просто нет механизма, о котором я знаю, который позволил бы мне делать такие объявления внутри класса.

Итак, теперь у меня есть желание иметь набор флагов, которые должны быть ограничены определенным классом, но я не могу использовать эти флаги в заголовке класса (например, инициализация по умолчанию, встроенные функции и т. Д.), Потому что я не могу включить ни один из механизм, позволяющий обрабатывать перечисление как битовые флаги до закрывающей скобки для определения класса. Или я могу определить все такие перечисления-флаги вне класса, к которому они принадлежат, чтобы затем я мог вызвать «преобразовать это перечисление в побитовый тип» перед определением пользовательского класса, чтобы в полной мере использовать эту функциональность в клиентский класс - но теперь битовые флаги находятся во внешней области видимости, а не связаны с самим классом.

Это не конец света - ничто из вышеперечисленного не является. Но все это вызывает бесконечные головные боли при написании моего кода - и мешает мне писать его самым естественным образом - то есть с заданным перечислением flag, который принадлежит определенному классу в пределах (ограниченного) этого клиентского класса, но с побитовым флагом -семантика (мой подход № 3 почти позволяет это - при условии, что все обернуто битовыми флагами - явно включать необходимую побитовую совместимость).

Все это все еще оставляет у меня досадное чувство, что это может быть намного лучше, чем есть!

Конечно, должен быть - и, возможно, есть, но я еще не понял, - подход к перечислениям, чтобы включить побитовые операторы на них, позволяя им объявляться и использоваться в рамках окружающего класса ...

У кого-нибудь есть вайп или подход, который я не рассматривал выше, который позволил бы мне "лучший из всех возможных миров" в этом?


Например

// union only for convenient bit access. 
typedef union a
{ // it has its own name-scope
    struct b
     {
         unsigned b0 : 1;
         unsigned b2 : 1;
         unsigned b3 : 1;
         unsigned b4 : 1;
         unsigned b5 : 1;
         unsigned b6 : 1;
         unsigned b7 : 1;
         unsigned b8 : 1;
         //...
     } bits;
    unsigned u_bits;
    // has the following valid bit-flags in it
    typedef enum {
        Empty = 0u,
        ReadOnly = 0x01u,
        Hidden  = 0x02u
    } Values;
    Values operator =(Values _v) { u_bits = _v; return _v; }
     // should be freely usable with standard bitwise operators such as | & ^ ~   
    union a& operator |( Values _v) { u_bits |= _v; return *this; }
    union a& operator &( Values _v) { u_bits &= _v; return *this; }
    union a& operator |=( Values _v) { u_bits |= _v; return *this; }
    union a& operator &=( Values _v) { u_bits &= _v; return *this; }
     // ....
    // they should be comparable to integral values such as 0
    bool operator <( unsigned _v) { return u_bits < _v; }
    bool operator >( unsigned _v) { return u_bits > _v; }
    bool operator ==( unsigned _v) { return u_bits == _v; }
    bool operator !=( unsigned _v) { return u_bits != _v; }
} BITS;


int main()
 {
     BITS bits;
     int integral = 0;

     bits = bits.Empty;

     // they should be comparable to integral values such as 0
     if ( bits == 0)
     {
         bits = bits.Hidden;
         // should be freely usable with standard bitwise operators such as | & ^ ~
         bits = bits | bits.ReadOnly;
         bits |= bits.Hidden;
         // the result of any bitwise operators should remain the named type, and not devolve into an integral
         //bits = integral & bits; // error
         //bits |= integral; // error
     }
 }

Реализация собственного набора битов с нижележащим целочисленным типом выбора не сложна. Проблема с enum заключается в том, что отсутствует необходимая метаинформация для адаптации к битрейту. Но все же при правильном метапрограммировании и признаках включения флагов возможно иметь такой синтаксис:

flagset<file_access_enum>   rw = bit(read_access_flag)|bit(write_access_flag);

Я использую подход FlagSet FlagSet для Code Review SE .

Ключ заключается в том, чтобы ввести новый тип, который будет служить «контейнером» для одного или нескольких включенных значений из фиксированного списка опций. Упомянутый контейнер представляет собой обертку вокруг bitset которая принимает в качестве входных данных экземпляры перечисления области видимости.

Благодаря типу enum он безопасен для типов и может выполнять побитовые операции с помощью перегрузки оператора, делегируя операции с битами. И вы все равно можете использовать перечисление scoped напрямую, если хотите, и если вам не нужны побитовые операции или для хранения нескольких флагов.

Для производства я внес некоторые изменения в связанный код; некоторые из них обсуждаются в комментариях на странице обзора кода.







enum-flags