Итак, мы находимся в C ++ 17, и до сих пор нет удовлетворительного ответа на действительно отличный интерфейс битовых флагов в C ++.
У нас есть enum
которые отбрасывают значения своих членов во вмещающую область, но неявно преобразуют в их базовый тип, поэтому могут использоваться как если бы они были битовыми флагами, но отказывались переназначаться обратно в перечисление без приведения.
У нас есть enum class
которая решает проблему области имен, так что их значения должны быть явно названы MyEnum::MyFlag
или даже MyClass::MyEnum::MyFlag
, но они неявно преобразуются в свой базовый тип, поэтому не могут использоваться в качестве битовых флагов без бесконечного приведения вперед и назад.
И, наконец, у нас есть старые битовые поля из C
такие как:
struct FileFlags {
unsigned ReadOnly : 1;
unsigned Hidden : 1;
...
};
Недостатком которого является отсутствие хорошего способа инициализации себя как целого — приходится прибегать к использованию memset или приведению адреса или тому подобное, чтобы перезаписать все значение полностью или инициализировать все сразу или иным образом манипулировать несколькими битами одновременно. Он также страдает от невозможности назвать значение данного флага, в отличие от его адреса — поэтому нет имени, представляющего 0x02, тогда как при использовании перечислений такое имя существует, поэтому с помощью перечислений легко назвать комбинацию из флаги, такие как FileFlags::ReadOnly | FileFlags::Hidden
— просто нет хорошего способа сказать так много для битовых полей.
Кроме того, у нас еще есть простой constexpr
или же #define
именовать битовые значения, а затем просто не использовать перечисления вообще. Это работает, но полностью отделяет битовые значения от базового типа битового флага. Возможно, это в конечном итоге не самый плохой подход, особенно если значения битовых флагов constexpr
в структуре, чтобы дать им свою собственную область имен?
struct FileFlags {
constexpr static uint16_t ReadOnly = 0x01u;
constexpr static uint16_t Hidden = 0x02u;
...
}
Таким образом, в настоящее время у нас есть много методик, ни один из которых не может дать действительно убедительный способ сказать,
Вот тип, который имеет следующие допустимые битовые флаги, имеет свою собственную область имен, и эти биты и тип должны свободно использоваться со стандартными побитовыми операторами, такими как | & ^ ~, и они должны быть сопоставимы с целочисленными значениями, такими как 0, а результат любых побитовых операторов должен оставаться именованным типом, а не переходить в интеграл
Все это говорит о том, что существует множество попыток создать вышеуказанную сущность в C ++ —
DEFINE_ENUM_FLAG_OPERATORS(EnumType)
который затем определяет оператор | & ^ ~ и связанные операции назначения, такие как | = и т. д.enable_if
метапрограммирование, позволяющее заданному перечислению преобразовывать в тип битового флага, который поддерживает отсутствующие операторы, и снова молча.bit_flags<EnumType> flags
а потом flags
имеет побитовую семантику. Чего это не может сделать, так это позволить перечисляемой базе фактически правильно обрабатывать побитовые операторы напрямую, поэтому вы не можете сказать, EnumType::ReadOnly | EnumType::Hidden
даже при использовании bit_flags<EnumType>
потому что само базовое перечисление все еще не поддерживает необходимые операторы. Мне пришлось в конечном итоге сделать то же самое, что и № 1 и № 2 выше, и включить operator | (EnumType, EnumType)
для различных побитовых операторов, требуя от пользователей объявить специализацию для метатипа для их перечисления, такого как 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 class
со следующими шаблонными операторами:
template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM operator |( ENUM lhs, ENUM rhs )
{
return static_cast< ENUM >( static_cast< UInt32 >( lhs ) | static_cast< UInt32 >( rhs ));
}
template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator |=( ENUM& lhs, ENUM rhs )
{
lhs = lhs | rhs;
return lhs;
}
template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline UInt32 operator &( ENUM lhs, ENUM rhs )
{
return static_cast< UInt32 >( lhs ) & static_cast< UInt32 >( rhs );
}
template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator &=( ENUM& lhs, ENUM rhs )
{
lhs = lhs & rhs;
return lhs;
}
template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator &=( ENUM& lhs, int rhs )
{
lhs = static_cast< ENUM >( static_cast< int >( lhs ) & rhs );
return lhs;
}
Если вас беспокоят вышеперечисленные операторы, просачивающиеся в другие перечисления, я думаю, вы можете инкапсулировать их в том же пространстве имен, в котором объявлено перечисление, или даже просто реализовать их на основе перечисления (я использовал макрос для тот). Вообще говоря, я считал это излишним, и теперь я объявил их в моем пространстве имен верхнего уровня для использования любым кодом.
Реализация собственного набора битов с нижележащим выбором целочисленного типа не является сложной Проблема с enum в том, что отсутствует необходимая метаинформация для адаптации к битам. Но все же при правильном метапрограммировании и признаках включения флагов возможно иметь такой синтаксис:
flagset<file_access_enum> rw = bit(read_access_flag)|bit(write_access_flag);